HandsOn Deep Learning for Finance
Implement deep learning techniques and algorithms to create powerful trading strategies
Luigi Troiano Arjun Bhandari Elena Mejuto Villa
BIRMINGHAM  MUMBAI
HandsOn Deep Learning for Finance Copyright © 2020 Packt Publishing All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. Commissioning Editor: Sunith Shetty Acquisition Editor: Reshma Raman Content Development Editor: Nazia Shaikh Senior Editor: Ayaan Hoda Technical Editor: Utkarsha S. Kadam Copy Editor: Safis Editing Language Support Editor: Sofi Rogers Project Coordinator: Aishwarya Mohan Proofreader: Safis Editing Indexer: Rekha Nair Production Designer: Nilesh Mohite First published: February 2020 Production reference: 1280220 Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK. ISBN 9781789613179
www.packt.com
Packt.com
Subscribe to our online digital library for full access to over 7,000 books and videos, as well as industry leading tools to help you plan your personal development and advance your career. For more information, please visit our website.
Why subscribe? Spend less time learning and more time coding with practical eBooks and Videos from over 4,000 industry professionals Improve your learning with Skill Plans built especially for you Get a free eBook or video every month Fully searchable for easy access to vital information Copy and paste, print, and bookmark content Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.packt.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at [emailprotected] for more details. At www.packt.com, you can also read a collection of free technical articles, sign up for a range of free newsletters, and receive exclusive discounts and offers on Packt books and eBooks.
Contributors About the authors Luigi Troiano, Ph.D., is an Associate Professor of Artificial Intelligence, Data Science, and Machine Learning at the University of Salerno (Italy), Dept. of Management and Innovation Systems. He is a coordinator of Computational and Intelligent System Engineering Lab at the University of Sannio and an NVIDIA Deep Learning Institute University Ambassador. He is also the chairman of the ISO/JTC 1/SC 42, AI and Big Data, Italian section. I express my gratitude to Vincenzo Benedetto and Francesco Gissi from the University of Sannio, for the valuable discussions we had and for their contributions to the code and experimentation in this book. I want to thank my awesome family, Lorenzo, Claudia, Ettore Jacopo, Mariangela, my wife, and everyone in the Packt Publishing team who supported me throughout this project. Thank you.
Arjun Bhandari is the Chief Investment Officer of a family office. His previous positions have been Head of Quantitative Strategies at ADIA (the largest sovereign wealth fund in the Middle East) and APG Investments (the largest pension plan in Europe). He has been deploying quantitative techniques in multiasset class investments for over 20 years, bringing this experience to bear on his most recent focus on machine learning applied to fund management.
Elena Mejuto Villa, Ph.D., is a data scientist in the Advanced Analytics team for Technology Services Consulting in a multinational firm in Milan. She completed her Master's Degree in Telecommunication Engineering at the University of Oviedo (Spain), and she received her Ph.D. in Information Technologies for Engineering from the University of Sannio (Italy). During her Ph.D., she researched the application of machine learning and signal processing techniques to timevarying signals/data in the fields of finance and gravitational wave data analysis.
About the reviewer Arunkumar N T attained M.Sc. (physics) and MBA (finance) degrees, and he is currently pursuing a CMA and a CS. He has over 20 years of experience in corporate life and 2 years of experience teaching MBA students. He is an entrepreneur and has previously worked for Airtel, Citi Finance, ICICI Bank, and several other companies. He has also worked on books such as Python for Finance and Data Visualization with R. I would like to thank my father for his support and being someone I can rely on, my mother for her trust in spite of my repeated failures, and Dr. Prabhanjan N T for his unconditional love and support.
Packt is searching for authors like you If you're interested in becoming an author for Packt, please visit authors.packtpub.com and apply today. We have worked with thousands of developers and tech professionals, just like you, to help them share their insight with the global tech community. You can make a general application, apply for a specific hot topic that we are recruiting an author for, or submit your own idea.
Table of Contents Preface
1
Section 1: Introduction Chapter 1: Deep Learning for Finance 101 How AI is redefining the financial services industry A brief history of AI in finance A shared background (before 1880) Computing probabilities (18801950) Automatic reasoning (19501980) Expert systems (19801990) Narrow AI systems (19902010) Machine learning at scale (2011today)
A first look at deep learning for finance
Data gathering Implementing an autoencoder Using TensorFlow to implement the autoencoder
Summary Chapter 2: Designing Neural Network Architectures Going through the basics Organizing neurons Representational spaces Learning the weights Regularization An interlude of history
Working with MLP
Neurons based on distance
Computing with tensors Training a network through backpropagation Understanding CNNs LeNet5, AlexNet, and others
Understanding RNNs
Long ShortTerm Memory (LSTM) Gated recurrent unit
Summary Chapter 3: Constructing, Testing, and Validating Models Building blocks of financial models Formulating a hypothesis for trading
9 10 12 13 15 19 21 25 27 29 30 32 38 41 42 43 48 51 55 61 62 63 71 73 75 78 81 92 96 97 99 100 101 103
Table of Contents
Selecting relevant financial models
Example – factor models for return prediction
Adding nonlinearity to linear models Simple neural networks to capture nonlinearity and preference shifts DeepNets to incorporate memory in the modeling process
Machine learning versus statistical models
Acquiring data from multiple sources Asynchronous Revised or backfilled Prone to manipulation Outliers
Implementing the model Keras TensorFlow Theano Microsoft CNTK PyTorch Caffe2 MXNet Chainer Torch Caffe Wrappers
Evaluating investment strategy
Commonly used statistics Commonly used financial metrics
Cumulative and monthly returns Information coefficient The information ratio and Sharpe ratio Maximum drawdown Sortino ratio
Tuning the model
Grid search Random search Bayesian optimization
Going live
Documenting investment strategy and code Transitioning to a production environment Paper portfolios Soft launch Go live!
Benchmarking
Benchmarking live data Benchmarking to model diagnostics
Summary
[ ii ]
105 106 107 108 108 109 109 110 110 111 111 112 112 113 114 114 115 116 117 118 119 119 120 122 122 122 123 123 124 126 127 128 128 131 132 135 138 138 139 139 140 141 141 142 143
Table of Contents
Section 2: Foundational Architectures Chapter 4: Index Replication by Autoencoders Replicating an index Data gathering Implementing a vanilla AE Data exploration and preparation Creating and fitting the model Evaluating the model
Replicating an index by using an AE Exploring some AE variants The denoising AE The sparse AE
Understanding deep AE Summary Chapter 5: Volatility Forecasting by LSTM Measuring volatility Types of volatility
Historical volatility Implied volatility Volatility index Intraday volatility Realized volatility
Loading the data Implementing the LSTM model Data preparation Creating and fitting the model Evaluating the model
Improving the model's performance Online learning Stacking layers Tuning the hyperparameters Visualizing results
Comparing LSTM with other models
RNN model The GARCH model Visualizing the cumulative squared error
Summary Chapter 6: Trading Rule Identification by CNN Trading signals with technical indicators Data handling Getting data from public sources Setting up the data
Hypothesis formulation and insample testing
[ iii ]
145 146 147 149 149 152 155 156 162 162 165 169 172 174 174 175 175 176 176 176 177 177 180 180 182 185 187 188 190 191 193 194 194 195 198 200 201 202 203 203 205 205
Table of Contents
Benchmarking alternative models
Benchmark 1 – simple trading rule Benchmark 2 – simple classification network
Constructing a convolutional neural network Modeling investment logic Selecting the network architecture Setting up the data in the correct format Training and testing the model
Summary
211 211 214 220 221 221 222 225 232
Section 3: Hybrid Models Chapter 7: Asset Allocation by LSTM over a CNN Modeling tactical asset allocation Defining our problem
Joint forecasting for an asset class Individual forecasting and bets
Setting up data Building a model
Understanding the deep learning model Implementing a CNNLSTM model Testing and validating our model Analyzing country models
Summary Chapter 8: Digesting News Using NLP with BLSTM Sentiment analysis for finance Representing text data – words to vectors Frequencybased word vectors Count vectorization TFIDF vectorization
Word embeddings Word2Vec
CBOW Skipgram
FastText GloVe
Data loading and splitting Implementing the BLSTM model Data preparation Creating and fitting the model Evaluating the model
Improving performance
Dealing with imbalanced classes Applying pretrained word embeddings
Considering separate decisions
[ iv ]
235 236 236 237 237 238 239 240 242 246 247 254 255 256 257 257 257 259 260 261 261 262 263 264 264 267 268 270 273 278 278 281 287
Table of Contents
Summary Chapter 9: Risk Measurement Using GAN Estimating value at risk Computing methods and drawbacks
Introducing generative adversarial networks Generative models Discriminative models Inner workings of GAN
Implementing a risk model using GAN Defining our model Implementing the GAN model
Benchmarking results Summary
293 294 294 295 297 298 299 299 300 300 301 311 315
Section 4: Advanced Techniques Chapter 10: Chart Visual Analysis by Transfer Learning Explaining what transfer learning is Understanding transfer learning What to transfer? When to transfer? How to transfer?
Using visual inspection in transfer learning for technical analysis What to transfer? When to transfer? How to transfer?
Implementing a transfer learning model Acquiring and formatting data Setting up data for the ResNet50 model Importing and training the model
Predicting test images Summary Chapter 11: Better Chart Analysis Using CapsNets Understanding CapsNets Modeling CapsNets
Dynamic routing between capsules Matrix capsules with EM routing Advantages of CapsNets Disadvantages of CapsNets
Constructing a CapsNet model Implementing the model Setting up data Training the model
Summary
[v]
317 318 320 321 321 321 322 323 324 324 324 325 327 330 335 337 338 339 341 342 343 343 344 344 347 347 352 362
Table of Contents
Chapter 12: Training Trader Robots Using Deep Reinforcement Learning Understanding Reinforcement Learning Deep Qlearning
Formulating the RL problem State Action Reward
Configuring the data
Loading the data Defining a trading strategy Input data Data preparation
Implementing a Robot based on Deep Qlearning Designing the agent
DQN Remember Experience replay Act
Training the agent Evaluating the model
Summary Further Research Chapter 13: What Next? Automating discovering and learning models from data Distributing computations across multiple computers and GPUs Distributed deep learning Data parallelism Model parallelism Layer pipelining
Frameworks for deep learning
Horovod Distributed TensorFlow models BigDL Elephas
Exploiting deep learning for highfrequency trading Using deep learning in other FinTech applications Payment transfer and processing Robo advisory Alternate currencies
Concerns about risks and the future of deep learning in finance Concluding remarks Other Books You May Enjoy Index
363 364 366 367 367 367 368 368 369 369 373 374 375 375 376 377 377 379 379 383 385 386 387 388 394 394 396 397 397 397 398 398 398 399 399 401 402 402 403 403 404 407 410
[ vi ]
Preface The possibility of predicting financial market trends has always fascinated all those who enter the world of finance for the first time, and even the most seasoned insiders are still captivated by the challenge of being one step ahead in predicting how the market will evolve and in being prepared. It may be months, weeks, days, hours, or seconds, but the challenge of projecting oneself into the future, of looking ahead beyond the limits of what we observe, is something ancient, magical in some ways. Benjamin Graham once wrote: "In the financial markets, hindsight is forever 20/20, but foresight is legally blind. And thus, for most investors, market timing is a practical and emotional impossibility." Many distinguished mathematicians, scientists, and economists have faced this challenge, often coming to the conclusion that financial markets are unpredictable. This statement certainly agrees with a large number of investors and practitioners. There are many economic, social, political, environmental, and emotional factors that intervene in the unpredictability of the markets. We have always tried to discover relationships between these factors and the market in a neverending attempt to look forward to the future. After all, when you think about it, the relationship between supply and demand reflects above all the comparison between antithetical visions of the future, the first optimistic, the second pessimistic. The market becomes, every day, at every moment, the ground of comparison between these visions. As impenetrable as they may be, these visions are formed and find strength in factual, concrete elements that, if put together, become pieces of a mosaic that's capable of, if not reading, at least of intuiting the future: a great challenge for the intellect on a par with those that have guided man to understand so many complex phenomena. In the ancient world, weather forecasts and the motion of the stars were associated with religion and spirituality. Weather events such as rain, tides, wind, cloudiness, floods, and drought were attributed to gods, who were worshiped to ensure good weather. Soon, independently in the world, different civilizations began to understand more, and to realize that everything obeyed a set of laws, which, although complex, were the basis of what they observed in the natural world. Of course, we could well say that the negotiation of the financial markets is very little concerned with the laws that govern the Universe, but on closer inspection, it still responds to principles of individual and collective behavior that, however complex, can still be decoded, interpreted, and related to the facts. The scientific and quantitative approach to finance has always looked at this possibility, trying to build more and more sophisticated models.
Preface
George Soros once said: "Taking this view, it is possible to see financial markets as a laboratory for testing hypotheses, albeit not strictly scientific ones. The truth is, successful investing is a kind of alchemy." A new and incredibly powerful tool to help create this alchemy is modern AI, embodied by deep learning. AI is considered by many to be the most important generalpurpose technology (GPT) of recent decades, and the key to the Fourth Industrial Revolution. AI promises to produce a radical transformation in many fields of technology, science, economics, and society as a whole. One of the sectors that looks at AI with attention is finance, in the quantitative field of investment strategies, negotiation, and risk assessment, but it's not the only one. There are many applications that AI will have in finance in the near future. The interest of the financial world in AI is not new. The arrival at modern AI technologies is an evolutionary path that starts from the adoption of the quantitative approach in finance at the beginning of the 20th century. The need to model the complex dynamics at the basis of price formation and to predict its trend in the future has led scholars and practitioners to experiment with increasingly sophisticated techniques of analysis. Today, deep learning receives a great deal of attention because of its incredible achievements in many other areas. Trained on massive amounts of complex and heterogeneous data, covering markets, economies, news, social trends, analysis, and more, they promise to set a new benchmark in the creation of quantitative models for finance, in relation to pricing, risk management, trading, and many other aspects of the industry. Hedge funds, investment banks, brokerage firms, analysts, investors, and most of the players are all affected by this transformation and the innovative approach that it achieves.
Who this book is for If you're a finance or investment professional who wants to lead the development of quantitative strategies, this book is for you. With this practical guide, you’ll be able to use deep learning methods for building financial models and incorporating them in your investment process. Anyone who wants to enter the fascinating domain of quantitative finance using the power of deep learning algorithms and techniques will also find this book useful. Basic knowledge of machine learning and Python programming is required.
[2]
Preface
What this book covers In this book, we will try to outline the path to this technology, pursuing an approach that is practiceoriented, while trying to keep methodological and scientific rigor in illustrating the principles that govern deep neural networks: Chapter 1, Deep Learning for Finance 101, tries to contextualize deep learning in finance
within the deep transformation of the global economy induced by the Fourth Industrial Revolution, and outline its evolution within a path that started in 1900 leading to modern quantitative finance. We will start with a practical example to understand the reasons for its use. Chapter 2, Designing Neural Network Architectures, on the other hand, is dedicated to
introducing the reader to the construction of neural structures, starting with the functioning of the unit, the neuron, and training it, and then moving on to the construction of more complex structures, up to convolutional and recurrent networks. Chapter 3, Constructing, Testing, and Validating Models, looks at the life cycle of a machine
learning/deep learning model in the financial field, analyzing all its phases, from its conception, implementation, and evaluation, to its deployment and decommissioning.
Chapter 4, Index Replication by Autoencoders, deals with the problem of index replication by
using a reduced subset of stocks/assets belonging to the index. An autoencoder is used to identify the group of stocks. Different variants of the model are investigated, reaching the best performance when a deep autoencoder is involved.
Chapter 5, Volatility Forecasting by LSTM, implements a multivariate model based on LSTM
in order to forecast the volatility of several stocks simultaneously. The proposed architecture is shown to outperform other models traditionally used.
Chapter 6, Trading Rule Identification by CNN, works on a detailed example of using a CNN
for pattern recognition and demonstrates that it is superior to other classification and heuristic measures using simple price and moving average data series.
Chapter 7, Asset Allocation by LSTM over CNN, delves into hybrid models where the CNN
model is used to capture crosssectional patterns, and they are propagated through time using LSTM. Asset allocation for country stock indices is used as an example to illustrate its effectiveness. Chapter 8, Digesting News by NLP with BLSTM, explores the sentiment analysis of the
financial market by processing news articles regarding certain stocks. This is done by using advanced techniques of NLP based on deep learning, specifically a bidirectional LSTM model.
[3]
Preface Chapter 9, Risk Measurement Using GAN, explores the area of estimating Value at Risk by
training a generative network to forecast return series for a number of stocks, preserving the joint distributional structure and the underlying nonlinearity in the relationship. Compared to traditional methods of Value at Risk estimation, this is a better methodology and works with minimal assumptions. Chapter 10, Chart Visual Analysis by Transfer Learning, elucidates the concept of employing
models that have been pretrained on a large amount of data and using them with minor modifications as a starting point to train with new datasets. This chapter uses technical analysis charts as would be used by technical analysts and aims to automate the process of feature detection. Chapter 11, Better Chart Analysis using CapsNet, develops on the earlier chapters and works
with a more complex model, which is a combination of several CNNs, to capture the spatial orientation of features relative to each other. This can be deployed to more closely decipher the various chart types used by technical analysts. Chapter 12, Training Trader Robots by Deep Reinforcement Learning, is aimed at training a
robot trader by backtesting its behavior using a trading strategy based on technical indicators, the strategy being unknown to the robot. The algorithm used in this chapter is Deep Qlearning, which relates to deep reinforcement learning. Chapter 13, What Next, concludes our journey, trying to answer some questions not fully
resolved and offering some food for thought about the use of this technology, outlining what the opportunities and risks are.
To get the most out of this book We expect the reader to have a basic knowledge of the principles behind machine learning and neural networks, although the latter are covered in Chapter 2, Designing Neural Network Architectures, and Chapter 3, Constructing, Testing, and Validating Models. A knowledge of Python and the KerasTensorFlow stack is also assumed. It is required that basic mathematical concepts are known, such as mathematical functions in multiple variables, vector calculation, and linear geometry. Competence in quantitative finance and trading is highly recommended, although not strictly necessary. You will need the Anaconda distribution installed on your computer in order to access packages regarding Keras, TensorFlow, pandas, and scikitlearn, among others. All code is available at https://github.com/PacktPublishing/HandsOnDeepLearningforFinance. Code examples have been tested using Keras on TensorFlow.
[4]
Preface
Download the example code files You can download the example code files for this book from your account at www.packt.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you. You can download the code files by following these steps: 1. 2. 3. 4.
Log in or register at www.packt.com. Select the Support tab. Click on Code Downloads. Enter the name of the book in the Search box and follow the onscreen instructions.
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of: WinRAR/7Zip for Windows Zipeg/iZip/UnRarX for Mac 7Zip/PeaZip for Linux The code bundle for the book is also hosted on GitHub at https://github.com/ PacktPublishing/HandsOnDeepLearningforFinance. In case there's an update to the code, it will be updated on the existing GitHub repository. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
Download the color images We also provide a PDF file that has color images of the screenshots/diagrams used in this book. You can download it here: https://static.packtcdn.com/downloads/ 9781789613179_ColorImages.pdf.
Conventions used There are a number of text conventions used throughout this book.
[5]
Preface CodeInText: Indicates code words in text, database table names, folder names, filenames,
file extensions, pathnames, dummy URLs, user input, and Twitter handles. Here is an example: "The result is template, which we can use to instantiate models with different hyperparameters." A block of code is set as follows: layer0 = Dense(20, input_shape=(10,))
Bold: Indicates a new term, an important word, or words that you see onscreen. For example, words in menus or dialog boxes appear in the text like this. Here is an example: "The Pearson Correlation Coefficient (PCC) is a statistical measure that calculates the linear relationship between two variables." Warnings or important notes appear like this.
Tips and tricks appear like this.
Get in touch Feedback from our readers is always welcome. General feedback: If you have questions about any aspect of this book, mention the book title in the subject of your message and email us at [emailprotected]. Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata, selecting your book, clicking on the Errata Submission Form link, and entering the details. Piracy: If you come across any illegal copies of our works in any form on the Internet, we would be grateful if you would provide us with the location address or website name. Please contact us at [emailprotected] with a link to the material.
[6]
Preface
If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.
Reviews Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you! For more information about Packt, please visit packt.com.
[7]
1 Section 1: Introduction In this section, you will be introduced to deep learning applied to finance, as we look at examples of deep learning applications in finance and the basics regarding model architectures and development, alongside an introduction to the software technologies that will be used in the remainder of the book. This section comprises the following chapters: Chapter 1, Deep Learning for Finance 101 Chapter 2, Designing Neural Network Architectures Chapter 3, Constructing, Testing, and Validating Models
1 Deep Learning for Finance 101 Finance, the science of managing money under conditions of uncertainty, represents a natural field for the application of Artificial Intelligence. Whereas conventional information processing is limited by assumptions of probability distribution and interpretation, Deep Learning technology helps us discover complex patterns in data in a modelfree manner so as to assist machines and humans in making better decisions. Artificial intelligence is the new steam engine (and big data is the new oil) that will cause the fourth industrial revolution. This is the new mantra that's being spread around the world by hightech firms, technology evangelists, and other influencers and decisionmakers. It is not the first time we have witnessed such enthusiastic expectations for AI. The first wave of artificial intelligence methods utilizing neural networks was accompanied by the 1980s with the advent of the backpropagation algorithm, though it made limited contributions, mainly due to expensive computing power. The new wave promises to have a far greater impact because of the enhancements that have been made to technological frameworks for both hardware and software, and greater acceptance in the societal and economic context. The key factor of this renewed belief in the field of deep learning is the astonishing results that it has achieved in a wide range of applications. The chapter will provide a first look at the world of deep learning when applied to finance, with a specific focus on the asset management industry. We will start by illustrating the longterm path that has led to modern AI in finance. We will look at several applications, including roboadvisors, index replication, technical analysis, risk management, automated trading, and more. This chapter will illustrate a simple example using autoencoders (which will be explained in more detail in Chapter 4, Index Replication by Autoencoders) for time series denoising and dimensionality reduction through a comparison with Principal Component Analysis (PCA). This chapter covers the following topics: How AI is redefining the financial services industry A brief history of AI in finance A first look at deep learning for finance
Deep Learning for Finance 101
Chapter 1
How AI is redefining the financial services industry Growth in revenue and profit share in the financial services industry has led to an attractive sideeffect: the availability of resources to sustain failure and the energy to innovate for the promise of supernormal gains. In the financial ecosystem, there are several areas in which AI can play an essential role. Anomaly detection either to save costs or to identify opportunities relies on pattern detection principles. Indeed, the renewed possibility of discovering complex patterns in the massive data that's now available to financial institutions and to reason about them can support both operational and investment activities. For instance, deep learning, and AI in general, help us to automate processes for fraud detection, transaction validation, advisory services, lending authorization, and trading in financial markets. In this book, we will focus on trading and portfolio management, but there are several other examples of applications in which AI is being successfully deployed nowadays in finance. One concern is fraud detection and prevention. Having an increasing amount of sensitive data stored online and accessible via the internet means that the risk for data leakage is higher now than ever before. This represents one major risk for data security. While, in the past, fraud detection relied on a complex set of rules, the modern approach aims to identify anomalies in behavior that may reveal potential and real security threats, thus alerting the security team to help them prevent or mitigate risks. The challenge is to build models that are able to identify threats accurately, but at the same time minimize the falsepositive situations where an alert is raised erroneously. The flexibility of AI systems is suitable for applications in the continuously evolving domain of creative fraudsters. It is thought that AI will be adopted to a large extent for this purpose within the next 510 years. Open Banking represents another security challenge that's faced by financial institutions in the age of Digital Transformation. New regulations, such as the revision of the Payment Services Directive (PSD2), push for full system integration. Such integration, which is made possible by extensive use of machinetomachine APIs, makes human intervention limited or no longer necessary at all. In such a context, AI algorithms can be exploited to their full potential when automating complex processes that previously required the involvement of people. One of these functions is user authentication. We are likely to get AI systems aimed at user identification through facial or voice recognition and other biometric data within the next 5 years.
[ 10 ]
Deep Learning for Finance 101
Chapter 1
Another application that is rapidly gaining traction is the automatic authorization of loans and insurance underwriting. When spreads are being reduced due to enhanced competition, speed and accuracy are becoming critical to reduce the cost of doing business. Especially in large companies, a massive amount of data exists concerning customers that can be used to train machine learning models to associate applicants to specific classes of risks. These models can also be used to monitor how risk and customer profiles evolve over time, from migrating from an initial and annual scorecard to realtime monitoring. The key benefit of algorithmic lending is an increase in the distribution of credit and reduced costs. This is akin to microlending but on a larger scale than can be afforded by small community networks. Emerging markets have been big beneficiaries of this activity, resulting in an increase in the wellbeing of the largely unbanked population. We expect that this kind of application will be widely adopted by the financial services industry in the near future. In general, AI is likely to be responsible for assessing and evaluating risks concerning different areas by allocating assets and incurring liabilities in order to maximize returns and minimize risks over a given time period. Recommender systems based on AI are another application that is resulting in huge benefits for service providers and consumers. Similar to what already happens in media and ecommerce, they can be employed to suggest products and services according to the customer's preferences and risk profiles, replacing or assisting humans in advising. Recommender systems are often associated with chatbots and conversational interfaces that rely on AI engines for robust Natural Language Processing. Banks and financial institutions have already started to use this kind of technology to assist their customers in remote banking and advisory services. Their adoption is expected to grow in the coming years. Nextgeneration recommender systems will have the capability of providing customized solutions. Until a few years ago, the term roboadvisor was unknown, but today it represents one of the leading technologies within the fintech space. Roboadvisors are AI software agents aimed at calibrating a financial portfolio with respect to the return goals and risk preferences of a customer. They are the most promising technology for supporting wealth selfmanagement in the future since customers are becoming more and more comfortable with managing savings and investments on their own, supported by automated and personalized analysis of their financial assets, without the need for human advisors, as in traditional banking.
[ 11 ]
Deep Learning for Finance 101
Chapter 1
However, the main area of interest in AI has to do with the application of modern machine learning to asset management and trading. Dating back to the 1970s, the automation of, and support for, decisionmaking at the trading desk has seen increasing investment in computing technology. This is due to the increased benefits of managing risks on a quantitative basis. The desire to augment human traders in order to shorten the time that it takes for decisionmaking in the context of portfolio trading (executing a large number of trades simultaneously) and highfrequency trading (HFT) (executing a trade in a fraction of a second) has led to a confluence of mathematics and finance, leading to the development of sophisticated algorithms. Models that are used by the industry are secret as they represent a strategic value for the participants. Their purpose is to discover patterns that are able to predict price movements and market trends, thereby making a profit by managing assets and trading them on the market. Previously, the finance industry relied on structured data that's represented by prices and volumes for statistical analysis, leaving unstructured data such as news flow, satellite imagery, and so on to human judgment. This is changing. Advances in natural language processing due to modern AI are enabling the automatic screening of news feeds and social media in order to anticipate market movements based on unstructured or soft data. Expression analysis and sentiment analysis are some of the most advanced tools that are used to understand the mood of the market in realtime. Together with the ability to understand different sources of information, such as economic data and a market order book, this will help produce a major breakthrough in AI in the financial domain.
A brief history of AI in finance The phrase Data is the new oil is generally first credited to Clive Humby, from his lecture at the ANA Senior marketer's summit, Kellogg School, in 2006. Regarding the definition, Humby points out that, like raw materials, data has no value unless it's appropriately transformed to provide useful information for the business. The financial world is inherently quantitative and datadriven. Cognitive capabilities such as planning, modeling, reasoning, problemsolving, and representation are particularly important, as these allow us to automate tasks and understand very large quantities of data at a high speed with efficiency and accuracy. Because of this, AI and Machine Learning, in particular, have always been of interest in the financial domain, long before deep learning came onto the scene.
[ 12 ]
Deep Learning for Finance 101
Chapter 1
A shared background (before 1880) Quantitative Finance and AI shared a common background long before they were recognized as independent research fields: probability. Probability makes it possible to estimate the likelihood of hypotheses on the basis of the evidence that's been gathered and allows us to model even the most complex links between variables that make up a model of reality. The notion of probability as the ability to be plausible is very old. It was in the seventeenth century that we got the first mathematical formulation. It is said (see Calculus, Volume II by Tom M. Apostol, 2nd edition, John Wiley & Sons, 1969) that the concept of probability arose in 1654 from a question about betting, which two famous French mathematicians tried to answer: Blaise Pascal and Pierre de Fermat. At that time, Antoine Gombaud, Chevalier de Méré, a French nobleman interested in gambling (as indeed many aristocrats of the time were), asked Pascal a question about a problem concerning a game. The game consisted of throwing a pair of dice 24 times. Gombaud's calculations seemed to contradict a wellestablished rule followed by gamblers: keeping a bet on "double six" in a 24throw game will produce a profit in the long run. To solve this problem, Pascal involved Fermat in a correspondence from which the mathematical principles underlying probability were derived. These principles were collected by Christian Huygens, scientist and teacher of Gottfried Wilhelm von Leibniz, in his book entitled De Ratiociniis in Ludo Aleae (On Reasoning in Games of Chance) of 1657. This was the first treatise on probability that dealt with problems associated with gambling. The question posed by Gombaud was of a financial nature. He questioned whether it was worth investing his own money in betting on where a double 6 would occur during a sequence of 24 launches, as generally believed by gamblers of the time. In other words would repeating this bet over time more than repay the capital invested? The calculations made by Gombaud led to the conclusion that this wouldn't be the case. Today, we know that the probability of having a double six is equal to , so the probability of not getting one double six in 24 throws is equal to the following:
So, Gombaud was right not to consider it advantageous to repeat this bet over time. In fact, it would have been particularly advantageous to bet against a widely held belief.
[ 13 ]
Deep Learning for Finance 101
Chapter 1
However, before Pascal and Fermat, other mathematicians had already been interested in some problems with random games. The Italian Gerolamo Cardano dealt systematically with probability theory as early as 1564, although his work entitled Liber de Ludo Aleae (Book on Games of Chance) would be rediscovered and published in 1663. Cardano was, himself, a gambler and a chess player. It is said that he was interested in using dice to make a profit in his own bets and therefore to repay the debts he himself had incurred. He recognized the mathematical relationship between favorable cases and unfavorable cases, from which he derived the definition of probability as a ratio between favorable cases and the totality of cases. He also came to recognize the rule for combining independent events, but he did not come to a precise formulation. He contributed other concepts to the field of combinatorial mathematics as well; he was among the first to introduce the concept of the binomial coefficient and the binomial theorem in the West. The application of probability theory to gambling made it extremely popular at the end of the 1600s and throughout the 18th century. Many illustrious mathematicians of the time contributed to its development, including Jakob Bernoulli, Isaac Newton, Thomas Bayes, AdrienMarie Legendre, Gottfried Wilhelm Leibniz, and Abraham de Moivre. Randomness plays an essential role in the definition of risk and in its management, as well as in the remuneration of capital. So, it is not surprising that probability theory represents the foundation of finance. But randomness is opposed to rationality and the ability to determine the course of actions through calculation and logic. This has always exerted a profound fascination and a challenge to human intellect. For this reason, over the course of many years, the study of probability profoundly influenced not only the financial domain but also the development of AI. Moreover, the same mathematicians who offered fundamental contributions to the development of probability theory also who have helped to define the basis of what computer science would become in the twentieth century. Consider, for example, Pascal, who, in 1640, built the first computing machine, Pascalina. Leibniz built the Stepped Reckoner, the first digital mechanical calculator, in 1694 and in 1702, he published his work on the formalization of logic and the binary number system. Leibniz was the first to recognize the equivalence between one/zero and true/false or activation/deactivation. In 1812, Pierre de Laplace published Théorie Analytique des Probabilités (Analytical Probability Theory), in which he introduced a series of new mathematical concepts and techniques. His work represented the first generalization of probability theory outside the field of random games, providing the basis for its application in a series of fields, including actuarial mathematics, statistical mechanics, social sciences, and error theory, which would see an important development in the nineteenth century.
[ 14 ]
Deep Learning for Finance 101
Chapter 1
Computing probabilities (18801950) Louis Jean Baptiste Bachelier, at that time a young PhD student at the University of Paris under the supervision of Henri Poincaré, published a doctoral thesis entitled Théorie de la spéculation (Theory of Speculation) in 1900. His work is widely recognized today as the origin of Quantitative Finance. In an attempt to model price trends on the Paris stock exchange, Bachelier was the first to study stochastic processes – in particular, Brownian motions – and their application to the valuation of stock options. Brownian motion is a stochastic model that's used to describe the random movement of particles immersed in a fluid. In short, the process assumes that a magnitude (in this case, the force to which a particle immersed in a fluid is subjected due to impacts with other particles) has Gaussian distribution and is independent of the value that was assumed in previous instants. Brownian motions owe their name to the 1827 work of the botanist Robert Brown concerning the microscopic study of movements that grains of pollen have in the water, without being able to offer a solution to how to model their motion. The first mathematical formalization of Brownian motions is due to the pioneering studies of Thorvald Nicolai Thiele. Thiele himself talks about "Brownian movements" as a tribute to Robert Brown in an 1880 work on the method of least squares entitled Sur la compensation de quelques erreurs quasi systématiques par la méthode des moindres carrés (On the compensation of some quasisystematic errors by the least squares method). Astronomer and director of the Copenhagen Observatory, Thiele was also a mathematician, statistician, and eminent actuary, founder and mathematical director of the Hafnia Insurance Company, and founder of the Danish Society of Actuaries. Bachelier ignored the work of Thiele and introduced many concepts that make up what is now known as stochastic analysis in an original way. The approach followed by Bachelier is very similar to the one that Albert Einstein used only a few years later, in 1905, when, unaware of Bachelier's work, he tried to provide an answer to the problem posed by Brown. The definitive formalization of the problem was given by Norbert Wiener in 1923, providing further elements for their analysis and development. For this reason, Brownian movements are also known as Wiener processes.
[ 15 ]
Deep Learning for Finance 101
Chapter 1
Fundamental contributions to the theory of Brownian movements were given later by Paul Levy (1939), who completed its formalization by giving life to the modern definition of random processes, and Kiyosi Itô (19421946), who derived a method of calculation for the resolution of stochastic equations. Bechelier's work significantly oriented the research agenda in the sector for much of the 20th century. In the 1960s, the application of Brownian motions to Finance was revised in light of the market efficiency hypothesis (according to which the price of an activity contains the whole past history of events that affected its value) in the most updated version represented by the Wiener process. It was then used as a basis for the valuation of derivatives from the wellknown Black and Scholes equation of 1973. The Brownian motion (or Wiener process) is the most wellknown mathematical representation of the random process known as random walk, a term introduced by Karl Pearson in 1905. Pearson was struggling with a problem that he believed to be of great interest and to which he was unable to provide an answer. He formulated it to Nature's readers in these terms: assuming that at time 0 a man is in position , and assuming that at each step he can decide to make a known length in a certain direction, what position will he be in at time ? His question was answered by Lord Rayleigh, who highlighted the similarity of the problem to a problem that was published by Rayleigh himself in 1880, entitled On the problem of random vibrations, and of random flights in one, two, or three dimensions. This led Pearson to conclude, ironically, that the most likely point is where a drunk (that is, a purely erratic direction) will be not far from where he started. At the time, Karl Pearson was already a famous English mathematician and biostatistician who had already provided monumental contributions to statistics, such as the correlation coefficient and its relation to linear regression, standard deviation, the Chisquared test, histograms, continuous probability distributions, the method of moments, principal component analysis (PCA), pvalue, Chi distance, precursors, and the special case of Mahalanobis distance. Pearson also formulated the statistical test of hypotheses and statistical decision theory. All of these are tools that underlie AI today, especially machine learning and its application to finance. Because of this, Pearson is considered the father of mathematical statistics, the discipline that combines the principles of probability theory with linear algebra, differential calculus, measurement theory, geometry, and stochastic analysis to define hypotheses, models, and experiments that go beyond simple data collection and description. This convergence between mathematical statistics and AI gave life to what we now call data science.
[ 16 ]
Deep Learning for Finance 101
Chapter 1
Major contributions to significance testing were also made by William Sealy Gosset, who introduced the Student's tdistribution, and Ronald Fisher, who introduced the concept of a "null" hypothesis, the significance test, and the analysis of variance (ANOVA). All of these are of fundamental importance in testing models in Finance. But there are two contributions to AI that were due to Fisher and that would later find a prominent role in machine learning: Fisher popularized the maximum likelihood estimation (MLE) in 1912, a method for estimating the model parameters that best fit data, that were already used by Gauss, Laplace, and Thiele. In 1936, he introduced linear discriminant analysis (LDA), which is largely used for classification, clustering, and as a basis for perceptrons and later for support vector machines (SVM). It is worth noting that Fisher started as an advocate of the Bayesian interpretation of probability in the early stages of his career, but he soon became a frequentist. The entire 20th century was the scene of a longrunning dispute between the Bayesian approach, which promotes epistemic probabilities based on a subjective assignment and was regarded as a measure of the "degree of belief" of an individual who assesses the unpredictability of a given situation, and the frequentist approach, where probability assignment is based on the relative frequency of occurrences when the experiment was executed a large number of times. The first half of the century was characterized by a prevalence of the frequentist approach over the Bayesian one. This situation changed in the second half of the century as the Bayesian approach gained renewed interest, especially due to its application in belief networks in AI. Another dispute that divided the world of mathematical statistics involved Fisher himself versus Jerzy Neyman and Egon Pearson (who was Karl Pearson's son). Pearson and Neyman proposed hypothesis testing as an alternative to Fisher's significance test. The hypothesis test was based on the contemporary formulation of null hypothesis H0 and an alternative hypothesis H1. The sample size and the levels of significance α and β must be decided in advance. If the null hypothesis can be rejected, the alternative hypothesis can be accepted. This depends on levels of alpha and beta, which are based on the errors of type I (false positive) and type II (false negative). Instead, the test of significance, which was made famous by Fisher but had already been in use since 1700 (Laplace used it to determine the human sex ratio at birth, for example), was based on the sole formulation of the null hypothesis H0 and on the calculation of the pvalue to decide if H0 could be rejected or not.
[ 17 ]
Deep Learning for Finance 101
Chapter 1
The confrontation lasted for several years and remained partly unresolved until the death of Fisher in 1962, partly due to the war that intervened in the meantime. In fact, starting from 1940, a hybridization of the two approaches was proposed, adopting hypothesis testing with the pvalue instead of comparing statistics with respect to their level of significance. Using the pvalue in the statistical verification of hypotheses was also made possible by a new invention: automatic information processing systems. The use of electromechanical data processing systems had already been highlighted at the 1880 US Census, where the rate of population growth due to immigration made it impossible to process data manually, so Herman Hollerith was commissioned to build a tabulator machine. Presented in 1890, it allowed users to process data for the 62,947,714 US inhabitants in just 6 years. The success and popularity of his invention persuaded Hollerith to found. The Tabulating Machine Company in 1896. These and three other companies merged in 1911 to form the ComputingTabulatingRecording Company (CTR), which was later rebranded International Business Machine (IBM) in 1924 by Thomas J. Watson, which the AI computer that was presented in 2011 is dedicated to. In 1929, Ben Woods, at that time director of the Department of Statistics at Columbia University, asked Watson to build a machine that was able to process an amount of data larger than ever before to find suitable statistical tests in education and psychology. Watson created a gigantic tabulator able to sum squares and compute powers, roots, and other arithmetic operations, which led the New York World, a famous newspaper owned by Joseph Pulitzer since 1883, to coin the term "Super Computing" in a column that appeared in 1930. The use of digital electronics for computing dates back to 1931 with a seminal paper by WynnWilliams titled The Use of Thyratrons for HighSpeed Automatic Counting of Physical Phenomena, followed by a series of papers published between 1934 and 1936 by Akira Nakashima introducing the switching circuit theory for Boolean logic. Influenced by this work, Claude Shannon published his fundamental work titled A Symbolic Analysis of Relay and Switching Circuits in 1938. These studies led professor John Vincent Atanasoff and graduate student Clifford E. Berry to design the first (nonprogrammable) digital electronic computer in 1937 at Iowa State University (ABC, the AtanasoffBerry Computer), followed by Z3, the first programmable computer presented in Berlin by Konrad Zuse in 1941. The Z3 machine was, in principle, Turingcomplete, although it was missing conditional branching, while ABC was not. The inspiration for a universal programmable abstract machine, based on a onedimensional storage tape, came to Alan Turing in 1935 as a result of a question posed by Maxwell Herman Alexander Newman during a lecture at Cambridge University. His fundamental paper titled On Computable Numbers, with an Application to the Entscheidungsproblem, was published in 1936. After he entered the Enigma codebreaking team at Bletchley Park in 1939, most of his work during WWII was secret until many years after the end of the war.
[ 18 ]
Deep Learning for Finance 101
Chapter 1
Alan Turing was strongly interested in the application of probability calculus to cryptanalysis. He experimented with the use of statistical analysis to master the codebreaking process by optimizing the trials of different possibilities. His results are described in two papers titled The Applications of Probability to Cryptography and A Paper on Statistics of Repetitions, which remained classified until 2012. After the war, we were witness to the development of several electronic computers. Turing contributed by designing the Automatic Computing Engine (ACE), an early electronic storedprogram device conceived in late 1945 but only implemented on a reduced scale in 1950. The first electronic generalpurpose computer was the Electronic Numerical Integrator and Computer (ENIAC), which was presented in 1946, followed by the Electronic Discrete Variable Automatic Calculator (EDVAC), whose internal architecture was designed by John von Neumann, who was inspired by the work of Turing.
Automatic reasoning (19501980) Finance was one of the first industries to make a profit based on the progress that was made in computing machinery during World War II. Computing appealed to the financial domain because of the potential of automation in analyzing and processing data for banking and accounting at a speed that was impossible for humans. Although, at the time, the focus was on the adoption of early automated methods to process commercial data known as Electronic Data Processing (EDP), banking transactions among them, AI was achieving its first successes by developing expert systems. EDP refers to the use of automated business data processing techniques, characterized by relatively simple and repetitive procedures being performed on large volumes of structured data. At this time, the era of AI was just about to start. In 1950, Alan Turing published a fundamental work titled Computing Machinery and Intelligence, where he formulates his famous AI test based on the imitation game and posed a provocative question as to whether or not machines can think. Actually, Turing's experiments with AI dated back to 1948, when he started to write Turbochamp, the first program to play chess at Victoria University in Manchester. After two years, in 1950, the program was completed, but it was unable to be run on a computer due to the lack of resources. So, in 1952, Turing executed the program manually in "a recognizable game of chess", as it was referred to by the chess champion Garry Kasparov. Turing died prematurely in 1954 at the age of 42.
[ 19 ]
Deep Learning for Finance 101
Chapter 1
The early years of AI saw the development of different approaches that were enabled by the new possibilities offered by computers. These approaches included different forms of reasoning that nowadays we categorize as symbolic, probabilistic, by search, and inductive reasoning. Because of this fragmentation, John McCarty coined the term "artificial intelligence" in 1955 in an attempt to gather a group of prominent researchers in the first AI conference. This was organized in summer 1956 at Dartmouth College by McCarty himself, Marvin Minsky, Claude Shannon, and Nathan Rochester. The participants would go on to be some of the most influential authors in the following years. Among them were Ray Solomonoff, who formulated the algorithmic probability and the algorithmic information theory; Olivier Selfridge, who wrote seminal papers on neural networks, machine learning, and pattern recognition; Arthur Samuel, who coined the term machine learning and applied its principles to the first checkersplaying program; Warren Sturgis McCulloch, who, along with Walter Pitts, proposed the first model of the artificial neuron; John Nash, known for his fundamental contributions to game theory; and Herbert Simon and Allen Newell, who created the Logic Theory Machine and, later, the General Problem Solver (GPS). At this time, the two main approaches to artificial intelligence were symbolic and connectionist. Marvin Lee Minsky was the person who best represented this confrontation. Minsky started with an interest in neural networks but later turned to the symbolic approach. The initial success that was gained by Simon and Newell's GPS led to great enthusiasm in the use of deductive reasoning and logic to build machines that were able to solve complex problems and even to solve the problem of general AI (Strong AI). This led to the development of expert systems that incorporated knowledge from different domains. The DENDritic ALgorithm (DENDRAL) was the first of these expert systems. Developed at Stanford University by Edward Feigenbaum and Joshua Lederberg in 1965, it was designed to support organic chemists in identifying the structure of unknown organic molecules by analyzing their mass spectra. Dendral was made of two subsystems: Heuristic Dendral and MetaDendral. The first was a rulebased engine designed to test candidate solutions proposed by the latter, thus forming the pair performance element/learning element that is nowadays considered fundamental for any machine learning solution.
[ 20 ]
Deep Learning for Finance 101
Chapter 1
From this experience, other expert systems were developed. Among them was MYCIN, an expert system developed at Stanford in 1972, released first as a doctoral dissertation by Edward Shortliffe in 1974 under the supervision of Buchanan and Cohen. It was used to support the medical diagnosis of severe infections and to recommend appropriate therapies. The knowledge base consisted of 500600 rules and the inference engine used an early implementation of backward chaining and other enhancements as a method to direct the reasoning towards the query. Its processing speed was about 50 rules per second. These expert systems were developed using LISP, which was, at the time, the most popular choice among AI researchers. LISP was designed by McCarthy in 1958 as a highlevel programming language designed to perform symbolic reasoning. This language is still very popular nowadays. Symbolic reasoning is based on Boolean logic and on the intrinsic dichotomy between true and false. This opposition is necessary for two fundamental principles of classical logic: The principle of the excluded middle: There is no other possibility other than true or false propositions. The principle of noncontradiction: No proposition can be simultaneously true and false. The need to overcome the strict bivalence of classical logic led Lofti A. Zadeh to propose a new paradigm in which statements were assumed true to some degree in the unit interval [0,1]. In 1965, Zadeh published Fuzzy Sets and in 1974, Fuzzy logic and its application to approximate reasoning, in which he proposed the formal elements and the principles of Fuzzy Logic. This approach gained a significant amount of interest in finance in the following years by George Soros and others. Despite the growing interest in expert systems and their applications, we had to wait until the 1980s to witness the diffusion of this technology in the financial domain. Before then, there was a significant development in probabilistic modeling and reasoning. Bayesian statistics is still a method that is largely used in finance today. Its popularity is mainly due to the contribution of Robert Schlaifer to the field with his studies concerning the Bayesian Decision Theory, or the application of Bayesian reasoning and statistics to decisionmaking by maximizing the posterior expectation of a utility function. After Schlaifer published his book called Probability and Statistics for Business Decision in 1959, there was a growing interest in it that led to an increasing number of publications in this area. For instance, in 1969, James E. Sorensen and John A. Tracy both proposed the application of the Bayesian approach to audit companies so that they could value their assets.
[ 21 ]
Deep Learning for Finance 101
Chapter 1
Expert systems (19801990) In the 1980s, expert systems in the financial domain were developed. There are different examples of applications in different areas. For instance, it was reported by John Durkin in 2002 that Dupont implemented over 100 expert systems with estimated savings of about $10 million per year. In a survey dated back to 1996, Durkin reported that business was the main area to adopt expert systems, with up to 300 implementations. One of the early applications was EDPXPERT, developed by William F. Messier Jr and James V. Hansen in 1981, to assist with the auditing of reliability of EDP controls. The result was that, although preliminary experimentation on two case studies was successful and encouraged for moving to production, the system did not perform well on audits. As the authors reported, the main limitation was because of the technical skill and the costs required to keep the system updated and running. Another area of large interest was in financial planning. PlanPower was an expert system developed by the Applied Expert System (APEX) in 1982 and was commercialized in 1986. Its purpose was to develop a personalized financial plan for individuals with a yearly income of over $75,000. Given the success of PlanPower, the company developed an extended version named Client Profiling System (CPS), which is used by insurance banks and brokers to address the financial plans of individuals with an income in the range of $25,000$200,000 per year. In 1986, PLANMAN was presented. This was developed by the Sterling Wentworth Corporation. The system used a knowledge base made up of over 7,500 rules to support professional financial planners in developing modular plans according to quantitative and qualitative personal data about the client. As output, the system was able to produce recommendations using natural language and graphics regarding income tax and cash flow, investment and portfolio, life insurance, disability income, retirement, education funding, and real estate. One of the first expert systems designed to assist traders in predicting markets was PROTRADER, proposed by Tingpeng Liang, University of Illinois, and K. C. Chen at the California State University. Released in 1986, this system was able to predict the 87 point drop in the Dow Jones Industrial Average on 11 September 1986 and the 508point drop on 19 Otober 1987.
[ 22 ]
Deep Learning for Finance 101
Chapter 1
The knowledge base consisted of rules designed to look mainly at the stock premium. Theoretically, the premium should be equal to the cost of holding the stock, or the interest rate minus the dividends. However, in practice, there is a misalignment between supply and demand in two different markets, with the future index moving above or below the underlying asset price. This difference can be used for arbitrage, transforming that difference into profit. Thus, PROTRADER was designed to monitor the premium in the market and, according to internal rules, decide on an optimal strategy and when to execute a trade. An example of a rule that's used by PROTRADER is as follows: rule51: if have money = yes and premium is positive and return is profitable and confidence = N then decision = execute buy program cf N.
PROTRADER included Fuzzy Logic in order to use linguistic quantifiers and qualifiers of some variables, such as the market mood in terms of bearish/neutral/bullish, which are provided by an expert judgment. It also implemented a learning mechanism in order to calibrate different rules. In financial jargon, the term "Bull" refers to an uptrend market, while "Bear" refers to a downtrend market. The origin of the terms is not very clear. According to some, the two expressions are related to the behavior that bears and bulls exhibited during fights that were organized in the Britian of Elizabeth I (15581603). The two animals, chained in the middle of a ring, were made to fight; with the bull when it attacks, tends to raise its horns and the bear, when it attacks, tends to use the claws in its front paws from top to bottom. The fights were the subject of bull and bear batting, which were called "bullbetting" and "bearbetting" respectively. The use of the term bear to indicate a bearbetting market became popular when the speculative bubble burst in 1720; this was linked to the demise of the South Sea Company, which, by virtue of a monopoly contract with the British Crown and an interest rate of 6% on the war debt it had bought, had begun to sell its own shares in 1711 at everincreasing prices. This bubble also involved Isaac Newton, who lost the huge sum of £20,000. He said, "I can calculate the movements of the stars, but not the madness of men."
[ 23 ]
Deep Learning for Finance 101
Chapter 1
From the 1990s, expert systems started to be used for fraud detection for analyzing a large amount of data and looking for discrepancies that would have revealed criminal intents. The FinCEN Artificial Intelligence System (FAIS) was one of these systems. Developed by the US Department of the Treasury in 1993, it started to be used to detect suspicious money laundering activities. The system was able to review more than 200,000 transactions per week and identify more than 400 cases of money laundering, covering a total of $1 billion over 2 years of operation. There are several other examples of applications in the financial domain. In 2002, Ljubica Nedovica and Vladan DevedzIica provided one of a few surveys focusing on the application of expert systems regarding company assessment, valuation, and reliability. They considered four classes of expert systems: Financial analysis of companies Identification of factors leading to the success/failure of a business Market analysis Knowledge acquisition in specific domains Often, expert systems are developed inhouse or for a specific customer. In only a few cases are they offered as commercial products. This is because of two reasons: the first is the secrecy that covers the implementation of such systems, especially when they are used for trading financial markets. Secondly, the different operational context requires systems to develop ad hoc knowledge that may not fit the needs of other customers. However, despite the interest and investments in them, expert systems did not fulfill their expectations. There are several reasons for this, some technical and others contextual. On the technical side, the approach based on knowledge and symbolic reasoning proved to be too rigid to adapt to the real world. The more precise the knowledge is, the more this characteristic becomes evident. Therefore, a higher degree of flexibility can only be gained at the cost of expanding the knowledge base. But this must be done coherently, without producing potential conflicts between rules, which leads the system to contradiction, even when a solution appears evident to humans. And this leads us to the contextual side. The maintenance and usage of expert systems was too complicated, requiring technical skills that were often not available on the clientside, resulting in a poor and sometimes frustrating user experience. Another source of disillusionment was the overexpectations given to expert systems, as they were able to solve any problem without considering the limitations and constraints at hand.
[ 24 ]
Deep Learning for Finance 101
Chapter 1
Narrow AI systems (19902010) In the late 80s, expert systems slowly stopped being used in Finance, as was also the case in other sectors. There are several reasons for this. The first reason is that they did not meet the high expectations that were created by the AI hype at that time; thus, latent skepticism became open criticism. For example, in a 1987 article titled The Morning After, which appeared on Forbes, the author Ruth Simon posed a provocative question about what happened to the expert systems that had promised to revolutionize business and industry. In 1991, after spending almost $1 billion, the Fifth Generation Computer System project, which was launched 10 years before in 1981 with great enthusiasm by the Japanese Ministry of International Trade and Industry, failed to meet its goals. The objective was to build an AI platform primarily based on Prolog, a language that was introduced in the 70s for running expert systems with firstorder logic knowledge bases, aimed at solving complex tasks such as having humanlike conversations, translating languages, understanding the content of images, playing complex games, and so on. This failure represented an iconic moment in describing the change of heart that was taking place in the business world with respect to AI. The technical roots of that change lay in the rise of personal computers, which resulted in them being more versatile and cheaper than more expensive list workstations. But the main reason for that failure lay in difficulties in maintaining complex rules bases, which were sometimes too rigid to model the volatile and critical nature of business logic. In addition, domainspecific rule engines started to be incorporated as components of vertical enterprise systems such as SAP, Siebel, and Oracle, thus abandoning the realm of AI and becoming part of proprietary business process automation suites. The result was a strong downsizing of public and private investments, with more than 300 companies in the field of AI stopping work because of bankruptcy or acquisition. The AI community split into different tribes that started to use other names to describe their work, such as Computational Intelligence, Soft Computing, Bayesian Reasoning, Knowledge Base Systems, Connectionism, Evolutionary Computing, Machine Learning, and so on. They did this in an attempt to differentiate themselves from the kind of AI that was based on Symbolic Reasoning, which had become very unpopular.
[ 25 ]
Deep Learning for Finance 101
Chapter 1
Since the mid80s, research has started to focus on very narrow specific tasks, abandoning the idea of solving the general AI problem. In finance, these solutions started to be embedded in other applications to solve specific datadriven problems generally characterized by uncertainty, complexity, and partial knowledge, such as granting loans according to customer profiling or to detect and prevent credit card fraud. Following this approach, in 1997, IBM's Deep Blue computer beat the world chess master Garry Kasparov. Deep Blue's internal logic was based on a search algorithm driven by heuristics. For this reason, it was not properly based on machine learning, but this achievement strongly marked that period of time and a rebirth of interest in AI, although from a different perspective. However, among its different approaches, machine learning has always been the approach that gained the most interest in the financial domain. Machine learning became very popular in the nineties after the Wall Street boom in the eighties attracted money and talented people, despite Black Monday in 1987 and the subsequent recession that lasted until the beginning of the nineties. In the 1990s, we saw the rise of Quants, professionals with a strong mathematical background, especially in stochastic modeling and analysis, who brought a quantitative approach to financial trading and risk management. Hedge funds such as LongTerm Capital Management (LTCM) started to use the first quantitative computer models that were partially based on statistical learning for trading assets on financial markets. LTCM, founded in 1994 by John W. Meriwether, the former vicechairman and head of bond trading at Salomon Brothers, had Myron S. Scholes and Robert C. Merton, as members of the board of directors, who shared the 1997 Nobel Memorial Prize in Economic Sciences. Their approach, like others in machine learning, relied on the Efficient Market Hypothesis (EMH) that was popularized by Eugene Fama in the seventies. According to EMH, these asset prices fully reflect all the available information. After some years of remarkable successes, LTCM went bust because of the Asian crisis in 1997 and the Russian default in 1998. However, the road was laid out and quantitative methods that were supported by machine and statistical learning became more and more popular in the following years. Techniques that have become popular over the years include Markov chains and other probabilistic graphical models; classifiers based on Naive Bayes classification, SVM, decision trees, MLP and RBF; genetic algorithms and other evolutionary computing methods; and Fuzzy Logic, to a minor extent. These techniques are now being employed to resolve different problems in finance, such as trading, portfolio optimization, risk analysis, and asset pricing.
[ 26 ]
Deep Learning for Finance 101
Chapter 1
Machine learning at scale (2011today) The advent of Big Data and GPU for massive parallel processing (MPP) has allowed us to build much more accurate models for different industries, Finance included. One milestone in this direction was represented by IBM's computer, known as Watson, which was able to answer questions that had been formulated in natural language in the open domain. Watson's internal logic is based on DeepQA, a solution that makes use of many techniques to deal with natural language processing, information retrieval, knowledge representation, automated reasoning, and machine learning in order to analyze a question, identify sources for the answer, make hypotheses, score them, merge and rank outcomes, and build an answer. Originally, Watson was designed as a competitor in a 2011 competition with humans champions on Jeopardy!, a popular quiz show. The competition was won by Watson. One of the most interesting aspects of Watson is that it was the first AI computer that relied on big data, and used encyclopedias, dictionaries, thesauruses, news feeds, literature, databases, taxonomies, and ontologies as sources of information, including Wikipedia, IMDB, DBPedia, WordNet, and Yago. Its architecture was based on a cluster made of 90 IBM Power 750 servers and 16 TB of memory, with Hadoop as a software framework to support big data management and distributed processing. The breakthrough, however, took place in 2012, when AlexNet, a convolutional neural network (CNN), competed in the ImageNet Large Scale Visual Recognition Challenge, achieving a performance of 10.8 percentage points above the best score reached up to that time. Convolutional neural networks have been investigated since the mideighties and then in the nineties, but the novelty of AlexNet resides in using graphics processing units (GPUs) to train a network whose depth was essential to reach its high performance. Since that moment, we have witnessed a Cambrian explosion in deep learning, made up of models, software frameworks, academic research, and startups, that gained the interests of large industries that faced complex problems such as computer vision, autonomous driving, drug discovery, medical diagnostic imaging, speech recognition, and translations.
[ 27 ]
Deep Learning for Finance 101
Chapter 1
Starting from 2013, hedge funds started to experiment with employing AI in their strategy for stock picking and fund management. Large banks, hedge funds, and traditional asset managers started to hire data scientists to develop machine learning algorithms in order to analyze tons of financial data points, but also looked at alternative data sources such as news feeds, blogs, social media posts, credit card transactions, and satellite imagery, in what Bloomberg has termed "an escalating technological arms race. According to the consultancy firm Tabb Group, spending on alternative data in the US alone will reach $400 million in the next 5 years. Machine learning algorithms, deep learning in particular, have also been employed for computational trading. An example is provided by Man Group, one of the largest hedge funds, which, since 2014, has started to use machine learning at AHL, their quant arm, in order to improve models from data and let them trade completely on their own. One of the most promising solutions is represented by deep reinforcement learning. This is a combination of reinforcement learning, the approach developed by Sutton and Barko in the late seventies and based on the reward mechanism in order to train an AI software agent to solve a task, and deep learning. It is capable of scaling the complexity of tasks that can be solved. This approach was pursued at DeepMind, a Londonbased AI company later acquired by Google in 2014, allowing them to defeat Lee Sedol, a 9dan world master of the Go game, in a challenge that took place in 2016. This was another event that marked the recent history of AI. In 2017, BlackRock, the world's largest asset manager, announced that they will rely more on big data and artificial intelligence in order to make them stronger with regard to their investment strategies. A few months later, JP Morgan and Chase Co. announced LOXM, their AI program, which had been trained on billions of historic transactions to offload large orders on the equity market by trading at optimal prices, reduced times, and minimal market swings. In research that was conducted in mid 2017 by Greenwich Associates, it was found that AI had already been adopted by 18% of firms in financial services and that 57% of financial services were planning to consider its adoption by the end of 2018. Eric Schmidt, former CEO of Google, believes that, in the future, no trading will be done without computers analyzing data from different sources, autonomously looking for market signals and trading on their own according to goals given by humans. The following diagram outlines the development that AI, ML, and DL have had over the years, all the way up to today:
[ 28 ]
Deep Learning for Finance 101
Chapter 1
Figure 1: Development of, and relationships between, artiﬁcial intelligence, machine learning, and deep learning
A first look at deep learning for finance As a first example of deep learning, we are going to look at a representation of time series data given by an autoencoder (AE) in a latent space with reduced dimensionality. This allows us to take into account the overall market. In this case, at each time , the price of an asset is assumed as the function of the overall market , assetspecific factors , and trading activity . There is a clear crossinfluence among assets that are nonlinear that depends on the market and the specific sector the asset belongs to. The purpose of an autoencoder is to learn the latent representation that minimizes the reconstruction error. Thus, an AE can be used to capture price movements in a smaller nonlinear latent space and use this representation in order to limit price components that are specific to a particular asset. In other terms, we aim to gather reconstructed timeseries data for each stock that passes through a latent space represented by the hidden layer, which is the "core" of the autoencoder. We expect that the resulting series will be more affected by the other stocks in the market ( ) but still take the assetspecific dynamics of the value ( , ) into account.
[ 29 ]
Deep Learning for Finance 101
Chapter 1
The following diagram depicts this process. At each time , the autoencoder maps the price movements given as input to the latent space, and from there it provides a reconstructed version that, due to the lower dimensionality of the representation, is depending on nonlinear relations between the asset prices:
Figure 2: The idea behind an AE working on time series data
The reasons for this will become clear in Chapter 2, Designing Neural Network Architectures, but for the moment, we'll only focus on its implementation in Keras and TensorFlow.
Data gathering In all analytics problems, the first stage is to collect the data useful for your analysis. This is what we will do in this section. 1. First, let's download data from the Dow Jones Industrial Average (DJIA) index. We will use pandas as the framework for collecting and manipulating data and pandas_datareader to gather the price series from Yahoo! Finance: import datetime as dt import pandas as pd from pandas_datareader import data as pdr
[ 30 ]
Deep Learning for Finance 101
Chapter 1
2. The table of DJIA symbols can be obtained via Wikipedia. In addition, we will also consider the index time series. The period of interest is 10 years, between January 1, 2008 and December 31, 2017: # Dow Jones 30 symbols_table = pd.read_html("https://en.wikipedia.org/wiki/Dow_Jones_Industrial_Av erage#Components", header=0)[2] symbols = list(symbols_table.loc[:, "Symbol"]) index_symbol = ['^DJI']
# Dates start_date = '20080101' end_date = '20171231'
3. After the price series and the index values have been retrieved, they are collected in a pandas DataFrame: # Download the data data = pd.DataFrame() # Clean all symbol labels and remove unavailable ones for i in range(len(symbols)): symbols[i]=symbols[i].replace(u'\xa0',u'').replace("NYSE:","") symbols.remove('DOW') # DOW data are unvailable on yahoo for i in range(len(symbols)): print('Downloading.... ', i, symbols[i]) # User pandas_reader.data.DataReader to load the desired data. As simple as that. data[symbols[i]] = pdr.DataReader(symbols[i], "yahoo", start_date, end_date)['Adj Close'] data_index = pdr.DataReader(index_symbol, "yahoo", start_date, end_date)['Adj Close']
4. Any missing values are cleaned up in the dataset and then it is saved into two separate files. The first, 'dj30_10y.csv', concerns stocks, while the second, 'dj30_index_10y.csv', concerns the index: # Remove the missing the data from the dataframe data = data.dropna() data_index = data_index.dropna()
[ 31 ]
Deep Learning for Finance 101
Chapter 1
# Save the data data.to_csv('dj30_10y.csv', sep=',', encoding='utf8') data_index.to_csv('dj30_index_10y.csv', sep=',', encoding='utf8') print(data.head())
Implementing an autoencoder In this section, we will implement an autoencoder using Keras. First, we import the libraries that are required to run our example: import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.preprocessing import MinMaxScaler from keras.layers import Input, Dense from keras.models import Model
The data is loaded and normalized by the MinMaxScaler in the range [0.1,0.9] in order to avoid saturation at higher/lower values of the series. It is applied to index and each series contained in data: # Load data = assets data =
data pd.read_csv('dj30_10y.csv', sep=',', engine='python') = data.columns.values[1:].tolist() data.iloc[:, 1:]
# Load index index = pd.read_csv('dj30_index_10y.csv', sep=',', engine='python') index = index.iloc[data.values.shape[0]:, 1:] # Normalize data scaler = MinMaxScaler([0.1,0.9]) data_X = scaler.fit_transform(data) scaler_index = MinMaxScaler([0.1,0.9]) index = scaler_index.fit_transform(index)
[ 32 ]
Deep Learning for Finance 101
Chapter 1
The autoencoder will be explained in more detail in Chapter 2, Designing a Neural Network Architecture. For the moment, let's imagine that it performs a mapping of the input space into the latent space (encoding) and then from the latent space back to the input space (decoding). So, the input and the output correspond to the number of assets. The size of the hidden latent space is set to n_core = N_COMPONENTS, where we assume that N_COMPONENTS = 3. As we will see, this is enough to explain more than 95% of the variance that can be seen in the data: # Number of components N_COMPONENTS = 3 ## Autoencoder  Keras # Network hyperparameters n_inputs = len(assets) n_core = N_COMPONENTS n_outputs = n_inputs
Defining an autoencoder is quite simple in Keras since it is made up of two Dense layers with an activation function on top of each, which is known as a sigmoid. The mapping from the input space to the latent space is generally referred to as "encoding", while reverse mapping from the latent space to the input space is referred to as "decoding": # Create model input = Input(shape=(n_inputs,)) # Encoder encoded = Dense(n_core, activation='sigmoid')(input) # Decoder decoded = Dense(n_outputs, activation='sigmoid')(encoded)
Given the two layers, we stack one over the other by means of the Model class constructor. Then, we compile the autoencoder. We set MSE as the loss function and adam as the optimizer: # define model autoencoder = Model(input, decoded) autoencoder.compile(optimizer='adam', loss='mse')
Since we are only interested in reducing the dimensionality, and not in performing predictions, our testing will be insample. This means that the data that's used for training will also be used to test the model: # Testing insample X_train = data_X X_test = data_X
[ 33 ]
Deep Learning for Finance 101
Chapter 1
Once the model has been defined, we run the training by means of fit, where we use X_train for both the input and the output. As we mentioned previously, we are interested in reconstructing the original data: # Training parameters epochs = 20 # Fit the model history = autoencoder.fit(X_train,\ X_train,\ epochs=epochs,\ batch_size=1,\ shuffle=True,\ verbose=1)
After the autoencoder has been trained, we test it insample over the X_test data: # Make AE predictions y_pred_AE_keras = autoencoder.predict(X_test) print('test loss: '+str(autoencoder.evaluate(y_pred_AE_keras, X_test)))
The y_pred_AE_keras variable represents a reconstruction of the price series. We can compare the result's we've obtained so far with respect to Principal Component Analysis (PCA) since this is a standard method for dimensionality reduction: PCA is a statistical procedure that produces a rototranslation of the axes of a multidimensional space in which the data are originally represented. The procedure first moves the origin of the axes to the average point of the data, i.e. the mean. Subsequently, it determines a first axis in order to maximize the variance of the data projected on it. Then, it determines a second axis, orthogonal to the first, so as to maximize the variance of the data projected on it. Following the same principle, it determines a third orthogonal to the first and second, and so on, until all axes are determined. ## PCA pca = PCA() pca.fit(X_train) print(pca.explained_variance_ratio_) cum_exp_var = np.cumsum(pca.explained_variance_ratio_) plt.plot(np.arange(1, 31), cum_exp_var) plt.xlabel('Principal components') plt.ylabel('Cumulative explained variance') plt.show()
[ 34 ]
Deep Learning for Finance 101
Chapter 1
In the following diagram, you can see that we've plotted the cumulative explained variance by varying the number of components, confirming that three components is enough to cover the variance of the data at 95%:
Figure 3: The cumulative explained variance in terms of the number of principal components
PCA components are used to reconstruct the time series, just like they are for the autoencoder: pca = PCA(n_components=n_components) pca.fit(X_train) y_latent_PCA = pca.transform(X_train) y_pred_PCA = pca.inverse_transform(y_latent_PCA)
[ 35 ]
Deep Learning for Finance 101
Chapter 1
We plot the series and save it as figures: # Plot series for stk in range(30): plt.figure() plt.plot(X_train[: ,stk], label='X') plt.plot(y_pred_PCA[: ,stk], label='PCA reconstruction') plt.plot(y_pred_AE_keras[: ,stk], label='AE reconstruction') plt.plot(ma, label='MA') plt.plot(index, label='DJIA') plt.title(assets[stk]) plt.legend() plt.savefig('Series/'+assets[stk]+'.png', bbox_inches='tight')
We use the simple average of all the prices as an additional reference: # Average ma = np.mean(data_X, axis=1)
Plotting the time series results in the following plots, where we're comparing the original price series, X, with the PCA, AE, and MA, along with the DJIA index:
Figure 4: A comparison of reconstruction oﬀered by diﬀerent methods
As shown in the preceding plots, the reconstruction offered by the autoencoder is generally closer to the index since it is affected more by the other stocks, while the PCA is closer to the original price series. Both are highly correlated to the other series, as shown in the following plots:
[ 36 ]
Deep Learning for Finance 101
Chapter 1
Figure 5: Correlation between the original time series and the reconstructions given by the diﬀerent methods
[ 37 ]
Deep Learning for Finance 101
Chapter 1
These plots can be obtained using the following code snippet: ## Correlations import seaborn as sns for stk in range(30): df = pd.DataFrame({'0Asset': data_X[:, stk], '2PCA': y_pred_PCA[:, stk], '1AE': y_pred_AE_keras[:, stk], '3Average': ma, '4DJI index': index.flatten()}) sns.set(style='white', font_scale=1.6) g = sns.PairGrid(df, aspect=1.4, diag_sharey=False) g.map_lower(sns.regplot, lowess=True, ci=False, line_kws={'color': 'black'}) g.map_diag(sns.distplot, kde_kws={'color': 'black'}) g.map_upper(corr)# dot(g.data, kde_kws={'color': 'black'}) plt.savefig('correlation/' + assets[stk] + '.png', bbox_inches='tight') def corr(x, y, **kwargs): # Calculate the value coef = np.corrcoef(x, y)[0][1] # Make the label label = str(round(coef, 2)) ax = plt.gca() # Add the label to the plot font_size = abs(coef) * 40 + 5 ax.annotate(label, [.5, .5,], xycoords=ax.transAxes, ha='center', va='center', fontsize=font_size)
In this example, we used Keras as a framework on which to implement the autoencoder. However, Keras is designed to use several computation backends, of which TensorFlow is the main one. Keras produces a translation of the specified model using its own API in the structures of the backend. Due to this, we can try the autoencoder implementation exercise using the backend API directly.
Using TensorFlow to implement the autoencoder Now, let's take a look at the implementation of the same autoencoder with TensorFlow. The input size and the number of units in the latent space are the same as they were previously: import tensorflow as tf from sklearn.utils import shuffle ## Autoencoder  TensorFlow # Network hyperparameters n_inputs = len(assets)
[ 38 ]
Deep Learning for Finance 101
Chapter 1
n_core = N_COMPONENTS n_outputs = n_inputs
First, we define the encoder and decoder layers: # Building the encoder def encoder(x): return tf.nn.sigmoid(tf.add(tf.matmul(x, w1), b1)) # Building the decoder def decoder(x): return tf.nn.sigmoid(tf.add(tf.matmul(x, w2), b2))
Then, we set the placeholders for the input and the output: A placeholder is simply a variable that we will assign data to at a later time.
# TF Graph input and output X = tf.placeholder("float", [None, n_inputs]) Y = tf.placeholder("float", [None, n_inputs])
We construct the model, linking the encoder to the decoder and outputting he latter as a prediction: # Construct model encoder_op = encoder(X) decoder_op = decoder(encoder_op) # Prediction y_pred = decoder_op
Afterward, we specify the weights and the bias for each layer by means of variables: A variable maintains its state in a graph.
# Create weights and biases for each layer initializer = tf.initializers.glorot_normal() w1 = tf.Variable(initializer([n_inputs, n_core])) w2 = tf.transpose(w1) b1 = tf.Variable(tf.zeros([n_core])) b2 = tf.Variable(tf.zeros([n_outputs]))
[ 39 ]
Deep Learning for Finance 101
Chapter 1
The target is set to X since we are interested in reconstructing the time series: # Targets are the same as input data y_true = X
Also, in this case, we choose MSE for the loss function and Adam for the optimizer: # Define loss to minimize the squared error mse = tf.losses.mean_squared_error(y_true, y_pred) # Define optimizer optimizer = tf.train.AdamOptimizer(lr).minimize(mse)
We are ready to train using the following parameters: # Training parameters lr = 0.01 epochs = 40 batch_size = 1
We start a new TensorFlow session. After the variable has been initialized, we perform autoencoder training. In particular, the following code implements a batch stochastic gradient descent algorithm, printing the loss at each epoch: # Start Training # Start a new TF session with tf.Session() as sess: # Initialize the network sess.run(tf.global_variables_initializer()) # Training for i in range(epochs): X_train1 = shuffle(X_train) for j in range(X_train.shape[0] // batch_size): batch_y = X_train1[j * batch_size:j * batch_size + batch_size, :] batch_x = X_train1[j * batch_size:j * batch_size + batch_size, :] _, loss_value = sess.run([optimizer, mse], feed_dict={X: batch_x, Y: batch_y}) # Display loss print('Epoch: %i > Loss: %f' % (i, loss_value)) # Make predictions y_pred_AE_tf = sess.run(decoder_op, feed_dict={X: X_train, Y: X_train}) print('Test Error: %f' % tf.losses.mean_squared_error(X_train, y_pred_AE_tf).eval())
[ 40 ]
Deep Learning for Finance 101
Chapter 1
As we can see, describing the model in TensorFlow may be more laborious than its specification with Keras. In TensorFlow, the model's description is narrowly aligned to its description in terms of the tensor's computation graph. This obviously offers great opportunities for optimization, control, and flexibility over the calculation structures. However, it also presents greater difficulty in its use and a slower learning curve. For this reason, Keras has gained great popularity among developers since we can use it to define models more naturally than with backends such as TensorFlow.
Summary In this chapter, we began our journey of deep learning for Finance. We learned how AI promises to transform the industry and how deep learning is just the last stop on the path to quantitative finance that begins from afar. As a first example, we illustrated the use of a type of network, called an autoencoder, to process some simple price time series. This was our first immersion in the world of deep learning models in finance, and one that we will explore in more depth in upcoming chapters. But before we go into that, we need to familiarize ourselves with some concepts behind the design of neural networks. In the next chapter, we will look at the principles of the processing unit that's at the heart of a neural network: the neuron. We will learn how to organize neurons into architectures and how to train them. Let's get started!
[ 41 ]
2 Designing Neural Network Architectures In the world of modern artificial intelligence, neural networks have taken on a central role thanks to their versatility in building complex models heavily driven by data, to solve a plurality of problems such as prediction, pricing, asset allocation, and others. In this chapter, we will explore the design, implementation, and training of neural networks more in depth, starting from the basics, with the aim of building shallow to deep architectures. We'll start with the basic processing unit: the artificial neuron. We will learn to understand its activation function, logic, and use. We will then move on to analyze its organization in layers and their representational capacity. We will complete its examination by setting the problem of learning its weights through the gradient rule. The first architecture to be introduced will be the MultiLayer Perceptron (MLP). We will use this architecture to illustrate the backpropagation algorithm. Then, we will introduce the Convolutional Neural Network (CNN) and learn its working principles. Finally, we will take a look at Recurrent Neural Networks (RNNs) and how they can be used in the processing of sequences and time series. This chapter will include the following topics: Going through the basics Working with MLP Computing with tensors Training a network through backpropagation Understanding CNNs Understanding RNNs
Designing Neural Network Architectures
Chapter 2
Going through the basics An Artificial Neural Network (ANN) is a generalpurpose model whose internal structure is made of interconnected processing units named artificial neurons or simply neurons. Each neuron implements a parametric mathematical function, generally taking the following form:
Here, the
function is named the activation function or transfer function, with
termed as potential and (fixing graphically represented as depicted in Figure 1:
) as bias. A neuron can be
Figure 1: An artiﬁcial neuron
In Figure 1, we can see the following: Different transfer functions can be employed. The choice depends on the intended use of the neural network. If the network is used for classification, the output provided by neurons is bistate. In that case, we can use this equation:
[ 43 ]
Designing Neural Network Architectures
Chapter 2
Here, we have the following: The potential, , incorporates the threshold given by . The value assigned to is generally , while for we generally opt for 0 or 1. In general, the activation function, , is monotonic nondecreasing, meaning that higher potential leads to higher activation output. The choice of and , corresponding to the Heaviside step function, is known as a perceptron, following the original idea proposed by McCulloch and Pitts in 1943. A brief history of neural networks is given later. The perceptron allows splitting the input space into two semispaces through a hyperplane whose equation is ; those points that are over the hyperplane are mapped to , while the points below to . In the case of , that is, bidimensional input spaces, the hyperplane is a line, as depicted in Figure 2:
Figure 2: A line splitting the input space into two semispaces (semiplanes)
[ 44 ]
Designing Neural Network Architectures
Chapter 2
The Heaviside step function changes abruptly when the threshold value, , is reached, as the function is discontinuous at the threshold. A smoother transition is offered by the piecewise linear function as the one given by the following equation:
The piecewise linear function equation is given for both cases of high/low pairs. The plots of the Heaviside step function and the piecewise linear function are outlined in Figure 3:
Figure 3: The Heaviside (left) and the piecewise linear (right) function plotted
Both of these functions have large flat regions that, as we will discuss later in this chapter, are not suitable for gradientbased learning procedures. For this reason, in practice, the sigmoid function is preferred to these two. A sigmoid function has a characteristic Sshaped curve and collects several special cases. The most prominent used as transfer functions in artificial neural networks are the standard logistic function:
[ 45 ]
Designing Neural Network Architectures
This is in the range of
Chapter 2
, and we have the hyperbolic tangent function:
The latter is a scaled and shifted version of the first, ranging from plotted in Figure 4:
. Both functions are
Figure 4: The logistic (left) and the hyperbolic tangent (right) transfer functions plotted
Another common choice is as follows: Although less popular today, is still in the range of . All of the preceding functions are strictly monotone increasing, which means the derivative is nonzero everywhere, and this is important to learn from data through a gradientbased optimization process. All of the preceding functions have in common two saturation regions (that is, high and low) and a quasilinear transition region between the two.
[ 46 ]
Designing Neural Network Architectures
Chapter 2
The following table, Table 1, reports a list of transfer functions: Name
Plot
Equation
Identity
Heaviside Step
Logistic
Hyperbolic Tangent
Arctangent
Rectified Linear Unit (ReLU)
[ 47 ]
Derivative
Designing Neural Network Architectures
Chapter 2
Leaky ReLU
Parametric ReLU
Exponential Linear Unit
SoftPlus
Organizing neurons A neuron alone can do little if space is not linearly separable. It is necessary to organize neurons in a network so that the neuron output reaches the input of other neurons. The scheme for the connection between neurons follows precise logic so that the network presents itself in a much more regular architecture than happens in nature, although both pursue the same goal: decompose the complexity of the network into modules to which they rely specific purposes and leave the connection between modules the possibility of integrating in a larger architecture. It is this "systemic" approach that characterizes modern artificial neural networks, in which a complex architecture is built starting from some elementary building blocks. In Table 1, we present the main ones. They are all available in Keras.
[ 48 ]
Designing Neural Network Architectures
Chapter 2
In Keras, these building blocks are represented as specializations of the Layer class that defines the common interface to them so that they can be integrated into a more complex architecture. This interface allows access to the input and output of the layer, its configuration, the weights, and other internal characteristics. The online documentation of Keras offers all of the useful details for an indepth study. Here, we prefer to describe the logic behind them. The following table, Table 2, shows the main layers available in Keras: Layer (class name) Dense Activation Dropout Flatten Input Reshape Permute RepeatVector Lambda
Description Regular denselyconnected neural network layer Applies an activation function to an output Applies Dropout to the input Flattens the input; does not affect the batch size Is used to instantiate a Keras tensor Reshapes an output to a certain shape Permutes the dimensions of the input according to a given pattern Repeats the input n times Wraps an arbitrary expression as a layer object
ActivityRegularization Applies an update to the cost functionbased input activity Masking SpatialDropout1D SpatialDropout2D SpatialDropout3D
Masks a sequence by using a mask value to skip timesteps Spatial 1D version of dropout Spatial 2D version of dropout Spatial 3D version of dropout
In addition, we have specific layers for the following: Convolution Pooling Locally connected Recurrent Embedding Merge Advanced activation Normalization Noise
[ 49 ]
Designing Neural Network Architectures
Chapter 2
Some of them will be described in the following section. All of them allow us to define and train complex architectures as those used for deep learning in finance. The simplest way is to organize the neurons in a row so that each neuron is provided with the input in full, as shown in Figure 5:
Figure 5: Dense layer
This is a Dense layer. To instantiate it, just define the number of units (that is, neurons) that it is made up of: layer1 = Dense(5)
In the example, we have created a dense layer composed of 5 units. This represents the output size of our layer. In Keras, the input size to a layer should not be specified because it depends on the size of the layers that precede it. If it is the first layer that is fed directly from the input data, then it is necessary to specify it: layer0 = Dense(20, input_shape=(10,))
In this way, we have created a dense layer that is fed by input 10 strings long.
[ 50 ]
Designing Neural Network Architectures
Chapter 2
Representational spaces There are several interpretations that can describe the behavior and purpose of a dense layer. In common, they express the ability of a dense layer to reproject a data point from a starting space into a representation space offered by the output of the neurons that compose it. This space can be larger or smaller, that is, it can offer a representation space with more components if the output size, layer.output_shape, is larger than the input size, layer.input_shape, or with a smaller number of components if the output size is smaller than the input size. This characteristic is an important property that the neural networks, and in particular the architectures of deep learning, exploit to be able, on the one hand, to better separate the data in a wider space, and then to reaggregate such characteristics exploded in reduced spaces. However, the projection offered by a dense layer is linear because, in essence, the operation that it carries out is the basic product, , where is the matrix of the weights (parameters) with which the input, , is given to the output units. Nonlinearities are necessary to obtain a better untying of components that are useful for solving a problem of classification, regression, or another type through a neural network. They are also fundamental to recompose these components in the output space of our interest. In the following code, we create a Sequential model: model = Sequential() model.add(Dense(20, input_shape=(10,), activation ='sigmoid')) model.add(Dense(5, activation='tanh'))
This allows you to place several levels in sequence: The first level is Dense composed of 20 units, while the input size is input_shape=(10,) and the transfer function is activation='sigmoid'. The output of this layer becomes the input of the next Dense layer, composed of 5 units, while the input size is automatically determined by the output size (20) of the previous layer. The nonlinear function used is activation='tanh'.
[ 51 ]
Designing Neural Network Architectures
Chapter 2
In Keras, we can also specify a layer dedicated to the precise application of a specific nonlinear function to its input. This is done by instantiating an Activation layer specifying which function to use.model.add(Dense(64)). So, a model equivalent to the previous one can be written as follows: model = Sequential() model.add(Dense(20, input_shape=(10,))) model.add(Activation('sigmoid')) model.add(Dense(5)) model.add(Activation('tanh'))
This model creates a simple MLP, which we will discuss in the next section. Here, we want to point out that the behavior of a simple model (only two levels in this case) can be reviewed in the light of what was described earlier. The neural network first takes the input represented in 10 dimensions and projects it in a nonlinear way into an internal space of 20 dimensions represented by the units of the first layer (hidden), each of which provides the value of the corresponding component. Subsequently, this decomposition into components is used to obtain the desired result. This certainly seems intuitive in the case of a classification problem: the decision of which class to assign to a specific entry is simpler if we appropriately represent our input in terms of characterizing elements that can be more easily connected to a class. Let's take an example: if we were to decide from a balance sheet whether the company was successful or was in danger of bankruptcy, it would first be useful to apply some more or less complicated formulas to the entries on the sheet, and then, based on the values obtained, take a decision. Both in the first step and the second, the simple addition (even weighed) of the items would help us: we need nonlinearity. The neural network does exactly the same thing, but with a great advantage. During the learning phase, it independently builds the best internal representation and the best way to use this representation to decide whether the company is successful or not. This approach is sufficiently general to be applied to a very wide range of problems, not only in finance. And the greater the capacity of a neural network to properly decompose a complex problem and to recompose from the components in which it is represented a useful, actionable decision, the wider will be its adoption.
[ 52 ]
Designing Neural Network Architectures
Chapter 2
To convince ourselves of the power of this concept, let's consider a problem such as that of determining a risk rate starting from some (even a few) values characterizing an asset. This is a problem of regression, that is, of determining a function (obviously not analytical in our case) able to determine this rate starting from the values of the asset. An MLP model still operates in the same way and determines the primary components in an internal representation space. Subsequently, it uses the set of components obtained to aggregate them into an output value that represents the risk rate. The ability of neural networks to learn a representation of data from an initial space, typically that of input, into a latent space of representation is well represented by autoencoders. An autoencoder is a neural network specifically designed to map inputs to a representational space that is smaller to compress information and reduce the sparsity of the origin space, so the reconstruction of data is close to the original input as much as possible. The architecture consists of two dense layers. The first one projects from the initial space to the latent space represented by the hidden layer. This operation is called encoding. The second is used to reconstruct the representation in the starting space from its representation in the latent space. In fact, the network exploits the property of universal approximation to provide a representation as close as possible to the original one. The scheme describing an autoencoder is outlined, as follows, in Figure 6 (bias inputs to units have been omitted for the sake of simplicity):
Figure 6: The internal architecture of an autoencoder
[ 53 ]
Designing Neural Network Architectures
Chapter 2
Its realization with Keras is extremely simple to build. Just define two Dense layers and link them together through Sequential, as shown by the following code: autoencoder = Sequential() # Encoder Layer autoencoder.add(Dense(128, input_shape=(784,), activation='relu')) # Decoder Layer autoencoder.add(Dense(784, activation='sigmoid')) autoencoder.compile(optimizer='adam', loss='binary_crossentropy') autoencoder.summary()
In the preceding code, we can see the following: The first layer maps a data point of 784 dimensions to a 128representational internal space. The second takes on this representation and tries to reconstruct the initial representation with 784 dimensions. The transposition into a space of representation can take place using a series of intermediate spaces, as shown in Figure 7:
Figure 7: Stacked autoencoder
[ 54 ]
Designing Neural Network Architectures
Chapter 2
This is an example of stacking multiple layers. The code that creates a stacked autoencoder is given as follows: autoencoder = Sequential() # Encoder Layers autoencoder.add(Dense(128, input_shape=(784,), activation='relu')) autoencoder.add(Dense(64, activation='relu')) autoencoder.add(Dense(32, activation='relu')) # Decoder Layers autoencoder.add(Dense(64, activation='relu')) autoencoder.add(Dense(128, activation='relu')) autoencoder.add(Dense(784, activation='sigmoid')) autoencoder.compile(optimizer='adam', loss='binary_crossentropy') autoencoder.summary()
Learning the weights Once the architecture has been defined in terms of dimensional and layer characteristics, it is time for training. Remember that the training serves to determine the combination of weights that maximizes the performance of the network in terms of accuracy and/or other criteria. We are interested in maximizing the network's performance, which is to minimize the error. Hence, ours is a problem of optimization aimed at identifying a configuration of parameters that minimizes the error. But which error? There are several ways to define and measure the error that a model, and therefore a network, makes. By borrowing the lexicon used in mathematical optimization problems and statistics, the definition of error, that is, the objective function we intend to minimize, is called a loss function. It maps the network instance, given the weights, to a value that measures the discrepancy between the expected output and the actual output for an instance of data.
[ 55 ]
Designing Neural Network Architectures
Chapter 2
Depending on the intended use and application of a network, there are different ways to express a loss function. In the following, Table 3, we report the most common that are made available in Keras: Name
Function (Keras)
Intended use
Equation
Binary Crossbinary_crossentropy Entropy
Classification
Mean Squared mean_squared_error Error
Regression
Mean Absolute Error (MAE)
mean_absolute_error
Regression
Huber Loss
huber_loss
Regression
Hinge
hinge
Classification
Cosine Similarity
cosine_proximity
Text
Table 3: Some popular options for loss functions
Whatever the loss function chosen, it outlines a landscape of research with the variation of the combination of parameters that, if it were perceptible in a threedimensional space, would seem to be in an impervious world made of cracks and walls. In such a space, we have to find the lowest point.
[ 56 ]
Designing Neural Network Architectures
Chapter 2
Let's imagine we're on top of a crater and want to reach the bottom of it where our base camp is, at night, while only having a flashlight to look around us. The most natural way we would think of would be to follow the steeper direction that promises to get us there as soon as possible: iteratively, at each step, we see in which direction the terrain descends most and we make a new step in that direction. This approach, called the Gradient Descent Algorithm (GDA), is what we implement to train a neural network. The name is because the gradient of the loss function indicates the direction in which to move. This process is well outlined in Figure 8, where the line indicates a possible path followed by GDA to reach the minimum function, . For the more curious, the function shown is the GoldsteinPrice function, a known artificial landscape used for testing optimization algorithms:
Figure 8: GDA (the path is only illustrative)
[ 57 ]
Designing Neural Network Architectures
Chapter 2
GDA belongs to the category of FirstOrder Optimization Algorithms. These algorithms minimize the loss function by using the gradient, that is, the firstorder derivative. It is also possible to use the second derivative based on Newton's method applied to the Hessian matrix of weights, which is defined as follows:
This approach presents the drawback of having to calculate a second derivative that generally results computationally more demanding. However, when the second derivative is known analytically, the secondorder optimization methods succeed in moving faster toward the minimum. The reason lies in the fact that methods based on the first order approximate the error function at a point with a tangent hyperplane, while methods of the second order, with a quadratic hypersurface: this allows us to move closer to the error surface when we update the weights at each iteration. However, to train a neural network, in practice, you always prefer to use a firstorder optimization method, because these methods are fast and accurate enough when applied to large datasets. The training algorithm is iterated several times on a dataset. Each complete step on the training set is called an epoch. Weight correction may be carried out on each sample that is supplied to the net. This allows an adjustment of the thoughts without having to wait for the end of an epoch. However, this has a cost, tied to the necessity to operate a correction of the weights before being able to proceed with the elaboration of the successive element. In massively parallel machines such as GPUs and TPUs, this approach can be particularly inefficient because it doesn't allow you to take full advantage of the parallelism on the data. That's why we prefer to use a subset of the training data that we call minibatch.
[ 58 ]
Designing Neural Network Architectures
Chapter 2
The number of elements in a minibatch is defined by the batch_size parameter. For example, if you have 1,000 elements that you want to organize in minibatches of 200 elements, we will have a total of 5 minibatches with batch_size = 200. The training algorithm will take the first 200 elements; it will calculate the average loss of them and, based on it, will determine the weight correction. The network so modified will be used to evaluate the 200 elements of the next minibatch and so on, until 1 epoch is completed after 5 minibatches, and so on to complete all of the epochs foreseen for the training. Obviously, we could decide to use a single minibatch that includes all of the data of the training set. However, this approach is not preferable for two reasons. The first is the limited capacity of the memory. A training algorithm requires all data in memory to be efficient; this is particularly true for GPUs and TPUs. Also, using smaller minibatches allows faster determination of the optimal. On the other hand, using larger minibatches helps to better stabilize the trajectory toward the minimum of the loss function. To understand this last aspect, just think that a correction made on a minibatch is designed to better accommodate the data of the minibatch, but it is not necessary that this correction is as effective for the next minibatch. Therefore, the larger the batch_size parameter, the less chance you have of making the wrong choice about data that is not part of the minibatch. Also, a larger batch_size parameter allows you to return to the same minibatch more frequently. The GDA applied to a minibatch is called Stochastic Gradient Descent (SGD). In Keras, we can instantiate SGD as follows: keras.optimizers.SGD(learning_rate=0.01, momentum=0.0, nesterov=False)
Here, learning_rate controls, as mentioned, the amount of weight correction proportional to the gradient. The remaining two parameters are useful to avoid being trapped in local minima. Indeed, momentum stabilizes the trajectory by accelerating it in the relevant direction and hence dampening the oscillations; nesterov controls whether to use simple gradient momentum (by default) or the Nestorov momentum. The optimizer can be specified when the model is compiled: sgd = optimizers.SGD(lr=0.01, decay=1e6, momentum=0.9, nesterov=True) model.compile(loss='mean_squared_error', optimizer=sgd)
[ 59 ]
Designing Neural Network Architectures
Chapter 2
Other options are available. The RMSprop optimizer is similar to the GDA with momentum. The novelty introduced by RMSprop is that it amplifies the effect of the gradient more when it is greater than 1. This allows you to accelerate the transition from one state of saturation to another state of the neurons: keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
The value of momentum is denoted by rho and is usually set to 0.9. With the RMSprop optimizer, it is recommended to leave this parameter at its default value. Adam stands for Adaptive Moment Estimation. This method makes the learning rate
adaptive to each parameter, using an exponentially decaying average of past squared gradients and gradients: keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999, amsgrad=False)
In addition to the usual learning_rate parameter, other parameters are beta_1 and beta_2, both within the unit interval and generally close to 1, and amsgrad to set a variant of this algorithm. Other optimizers supported by Keras are AdaGrad (Adaptive Gradient, 2011) and its variant AdaDelta (2012): keras.optimizers.Adagrad(learning_rate=0.01) keras.optimizers.Adadelta(learning_rate=1.0, rho=0.95)
They're both gradientbased algorithms. Adagrad uses an independent learning rate for each parameter to link it to its variation over time. The basic idea is to enable higher corrections for parameters that are subject to fewer corrections, and smaller corrections for parameters that are subject to more corrections. The initial rate is set equal for all parameters based on learning_rate, passed as a parameter in its instantiation. The value of 0.01 is, in fact, a widely accepted standard and is not recommended to be changed. This makes AdaGrad essentially hyperparameterfree. AdaGrad has proven to be more robust than SGD, especially in largescale DNN training and especially with scattered data such as natural language and image processing.
[ 60 ]
Designing Neural Network Architectures
Chapter 2
Adadelta is an extension of AdaGrad that seeks to compensate for the decay of the
learning rate inherent in its mathematical formulation. In fact, AdaGrad tends to reduce the learning rate of all parameters to zero after a certain number of iterations, effectively stopping the learning process of the network. This is due to the accumulation of past squared gradients that acts on the progressive reduction of the learning rate. AdaDelta restricts this accumulation to a more recent time window, using an exponential moving average whose decay rate is rho, so that older gradients weigh less and less and are replaced by newer gradients.
Regularization A wellknown threat to those who work in finance in mathematical and statistical modeling is represented by illposed problems and model overfitting. This problem also emerges in the training of neural networks. A solution is offered through regularization. In the case of neural networks, this happens when a parameter is weighed too much, and ends dominating the output of a neuron or layer. To overcome the problem, the regularization introduces a penalty factor to the loss function that helps to avoid it. In this case, the penalty acts on large weight values. Therefore, larger values are corrected not only when they produce incorrect predictions (underfitting) but also when they tend to dominate a correct decision (overfitting) on the training set. In Keras, penalties are applied at the layer level. How this happens depends on the layer, but the Dense, Conv1D, Conv2D, and Conv3D layers have a common interface. They accept as part of their specification instances of keras.regularizers.Regularizer for the kernel_regularizer, bias_regularizer, and activity_regularizer arguments. For instance, we can add regularizers to the autoencoder definition, as follows: from keras import regularizers autoencoder = Sequential() # Encoder Layer autoencoder.add(Dense(128, input_shape=(784,), activation='relu, kernel_regularizer=regularizers.l2(0.01), activity_regularizer=regularizers.l1(0.01))) # Decoder Layer autoencoder.add(Dense(784, activation='sigmoid' kernel_regularizer=regularizers.l2(0.01), activity_regularizer=regularizers.l1(0.01)))
[ 61 ]
Designing Neural Network Architectures
Chapter 2
Default regularizers in Keras are keras.regularizers.l1 (LASSO, short for Least Absolute Shrinkage and Selection Operator), keras.regularizers.l2 (Ridge), and keras.regularizers.l1_l2 (both). Also, you can create your own regularizers.
An interlude of history Neural networks have been developed over the last 75 years through a multidisciplinary research effort involving neuroscientists, mathematicians, psychologists, computer scientists, and electronic engineers. The first model of an artificial neuron is due to Warren McCulloch, a neurophysiologist, and Walter Pitts, a mathematician, who in 1943 defined a circuit known as Threshold Logic Unit (TLU), or Linear Threshold Unit (LTU), able to exhibit a static behavior similar to that of a natural neuron. In 1949, Donald Hebb, a psychologist, used the McCullochPitts model at the base of a network able to learn based on the interaction of neighboring neurons. This method is known today as Hebb's Rule. The first computational simulation of a neural network is due to Nathaniel Rochester, an engineer, who implemented the first software implementation of a neural network on IBM 704. Rochester, together with McCulloch and other pioneers of the time, took part in the Dartmouth Summer Research Project on Artificial Intelligence in 1956. In 1958, Frank Rosenblatt proposed the perceptron network model, which could classify continuous values into two or more classes. In 1959, Multiple ADAptive LINear Elements (MADALINE) was proposed, the first commercial application of a neural network by Bernard Widrow and Marcian Hoff. In 1969, Marvin Minsky and Seymour Papert published the book titled Perceptrons, in which they highlighted some limits that, according to the authors, limit the application capacity of neural networks. This will lead to a period of disaffection with the technology of artificial neural networks and research will be greatly reduced throughout the '70s. We will have to wait until the '80s to review the revival of interest in this technology, first thanks to the work of John Hopfield on associative memories of 1982 and the milestone represented by the backpropagation algorithm presented by David E. Rumelhart, Geoffrey E. Hinton, and Ronald J. Williams in 1986.
[ 62 ]
Designing Neural Network Architectures
Chapter 2
Working with MLP A single neuron can do very little. As we have seen, in the case of classification, it can discriminate between two classes if they are linearly separable. However, data is rarely not so easily separated. In these cases, we can use a network composed of a sequence of fully connected layers, like the one shown in Figure 9:
Figure 9: MLP
This type of network is known as MLP because each layer is composed of perceptrons. The entrance is provided to all the neurons of the first level, which considers them according to the corresponding weights. The output of the first hidden layer is supplied to each neuron of the second, and so on, until the outputs of the last hidden layer are supplied to the output layer whose neurons provide the network output. The network shown in Figure 9 can easily be implemented in Keras. To do this, we need a Sequential object to which are added Dense, that is, fully connected, layers of neurons: model = Sequential() model.add(Dense( 7, activation='sigmoid', input_dim=4)) model.add(Dense(10, activation='sigmoid')) model.add(Dense( 1, activation='sigmoid'))
[ 63 ]
Designing Neural Network Architectures
Chapter 2
The number of neurons that make up each layer and the activation function are the hyperparameters that we must specify to fully describe the architecture. So, the first hidden layer is made of 7 neurons, the second of 10, and the output layer of 1 single neuron. All neurons are activated by a sigmoid function. The first layer assumes 4 as the input size, that is, the number of dimensions of the input space. Hyperparameters are those parameters that are set before the learning process and therefore define the structure of the model, such as the number of units and the activation function in relation to each layer in an MLP architecture, but also as a loss function used to measure the performance of the model and the training characteristics, such as the algorithm to be used and the related parameters such as the learning rate and the dropout. Otherwise, the other parameters, such as the weight matrices in an MLP model, are derived during the training process. Let's consider a full example of how to define, train, and test an MLP classifier. The problem we consider concerns the possibility of determining whether an economicfinancial framework can lead to a crisis. The dataset, available on Kaggle, contains data on the economic and financial crisis in 13 African countries in the period 18602014: 1. As a first step, we include useful modules from Keras, pandas (dataset management), NumPy (numeric processing), and scikitlearn (data preparation): import pandas as pd import numpy as np from keras.models import Sequential from keras.layers import Dense from sklearn import preprocessing from sklearn.model_selection import train_test_split from sklearn.utils import shuffle
2. Then, we retrieve the data and shuffle the points to avoid potential bias and avoid zero due to the order in which data was collected and saved in the file: # data gathering available # at https://www.kaggle.com/chirin/africaeconomicbankingandsystemiccrisisdata df = pd.read_csv('african_crises.csv') df = shuffle(df, random_state=11) df.reset_index(inplace=True, drop=True) # data labeling df['banking_crisis'] = df['banking_crisis'].replace('crisis',np.nan)
[ 64 ]
Designing Neural Network Architectures
Chapter 2
df['banking_crisis'] = df['banking_crisis'].fillna(1) df['banking_crisis'] = df['banking_crisis'].replace('no_crisis',np.nan) df['banking_crisis'] = df['banking_crisis'].fillna(0) # data cleaning and scaling df.drop(['cc3','country'], axis=1, inplace=True) df_scaled = preprocessing.scale(df) df_scaled = pd.DataFrame(df_scaled, columns=df.columns) df_scaled['banking_crisis'] = df['banking_crisis'] df = df_scaled
3. We organize the data so that the input and output to the network are defined, and the dataset is divided into training and testing sets: # defining the input data, X, and the desired results, y X = df.loc[:,df.columns != 'banking_crisis'] y = df.loc[:, 'banking_crisis'] # breaking data into training data, validation data, and test data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, shuffle=False)
The model is defined in terms of layers composing the MLP architecture as follows. Then, the model is made ready for training by the compile method. 4. At this step, we can specify the optimizer and the loss function used for learning the weights. Also, we can choose which metrics to use for recording the model performance along the training epochs: # constructing a Multilayer Perceptron model = Sequential() model.add(Dense(32, activation = 'relu', input_dim = 11)) model.add(Dense(16, activation = 'relu')) model.add(Dense(1, activation = 'sigmoid')) model.compile(optimizer='rmsprop',loss='binary_crossentropy',metric s=['accuracy'])
5. We train the network providing the input/output training set and the number of epochs. Please note the verbose parameter that rules the output provided along the training phase: # training the network model.fit(X_train, y_train, epochs=10, verbose=2)
[ 65 ]
Designing Neural Network Architectures
Chapter 2
In the following, you can see the output. During the training, we will observe how the model performers better along the epochs. This is the essence of learning for a neural network: Epoch 1/10  1s  loss: Epoch 2/10  0s  loss: Epoch 3/10  0s  loss: Epoch 4/10  0s  loss: Epoch 5/10  0s  loss: Epoch 6/10  0s  loss: Epoch 7/10  0s  loss: Epoch 8/10  0s  loss: Epoch 9/10  0s  loss: Epoch 10/10  0s  loss:
0.5336  acc: 0.8612 0.4272  acc: 0.9040 0.3607  acc: 0.9129 0.3078  acc: 0.9158 0.2614  acc: 0.9188 0.2211  acc: 0.9276 0.1868  acc: 0.9409 0.1585  acc: 0.9483 0.1342  acc: 0.9645 0.1145  acc: 0.9764
The preceding block shows the output of model.fit along the different epochs in terms of time, loss, and accuracy. 6. The model can be scored over the portion of data used for testing. This approach for scoring the model is named outofsample: scores = model.evaluate(X_test, y_test) print ("Testing Accuracy: %.2f%%\n" % (scores[1]*100))
The model.evaluate method returns the loss value and metrics value for the model in test mode. Then, the outcome is printed on the screen, resulting in the following output: 32/212 [===>..........................]  ETA: 0s 212/212 [==============================]  0s 16us/step Testing Accuracy: 96.23%
[ 66 ]
Designing Neural Network Architectures
Chapter 2
7. If we score the model on the training set, we perform the insample scoring approach: # scoring it on the data it trained on as well as test data scores = model.evaluate(X_train, y_train) print ("Training Accuracy: %.2f%%\n" % (scores[1]*100))
As we can notice, this result is expected to be better: 32/677 [>.............................]  ETA: 0s 677/677 [==============================]  0s 64us/step Training Accuracy: 98.08%
MLP can also be used for regression, or to identify a model that expresses a quantity as a function of other variables. Building regressive models can be useful in a variety of problems in finance, for example, for asset pricing, risk estimation, or trend prediction. The possibility of converging in a problem of regression is given by the Universal Approximation Theorem (UAT). This theorem of fundamental importance states that a feedforward network with a single hidden (nonlinear) layer having a finite number of neurons, followed by a (linear) dense layer, can approximate any function within a given precision. Formally, the theorem can be expressed as follows: Let function, ,
, continuous, the function we aim to approximate by a . For any precision , there exists , , so that we have an absolute error, , for any with the following:
Here, is a nonconstant, bounded, and continuous activation function, providing the output of the hidden layer, and , the potential value of each of the hidden units. Each output, , of the hidden units is combined by a dense layer to get
.
[ 67 ]
Designing Neural Network Architectures
Chapter 2
This theorem has been proven first by George Cybenko in 1989 for sigmoid activation functions. Later, in 1991, Kurt Hornik showed that this theorem is valid for any choice of activation function under mild conditions. Finally, Lu et al. in 2014 and Hanin in 2017 proved the theorem to be valid for deep neural networks. However, the UAT just assures us that a feedforward network able to approximate at best our function exists, but it does not provide any hints on how to determine it. To build a regression model, you can use any activation function. However, the identity and ReLU function are much more common, as shown in the following example. 8. In this case, we will build a neural network for predicting the housing price in the Boston area using a set of 13 characteristics associated with buildings on sale. The dataset is part of samples made available by Keras. The procedure is the same. We import the module for plotting, in addition to those imported before. Then, we load, scale, and prepare the train and test sets: from keras.datasets import boston_housing import matplotlib.pyplot as plt # load dataset (X_train, y_train), (X_test, y_test) = boston_housing.load_data() # first we fit the scaler on the training dataset scaler = StandardScaler() scaler.fit(X_train) # then we call the transform method to scale both the training and testing data X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test)
9. The model is defined and compiled. In particular, we build an MLP made of three Dense layers, respectively of 8, 32, and 1 units. The first two layers make use of ReLU as an activation function, while the last uses the default standard logistic. The metric used for evaluating the model is MAE and the RMSprop optimizer: # create model model = Sequential() model.add(Dense(8, input_shape=[X_train.shape[1]], activation='relu')) model.add(Dense(32, activation='relu'))
[ 68 ]
Designing Neural Network Architectures
Chapter 2
model.add(Dense(1)) # compile model model.compile(loss='mean_squared_error', optimizer='rmsprop', metrics=['mae']) model.summary()
The summary method is useful to see an overview of the structure of the model just created, as shown in the following: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_1 (Dense) (None, 8) 112 _________________________________________________________________ dense_2 (Dense) (None, 32) 288 _________________________________________________________________ dense_3 (Dense) (None, 1) 33 ================================================================= Total params: 433 Trainable params: 433 Nontrainable params: 0 _________________________________________________________________
10. Finally, we train and score the model, and then we plot the MAE along the different epochs: # model training and scoring history = model.fit(X_train_scaled, y_train, validation_split=0.2, epochs=100) print(model.evaluate(X_test_scaled, y_test)) # summarize history for accuracy plt.plot(history.history['mean_absolute_error']) plt.plot(history.history['val_mean_absolute_error']) plt.title('model mean absolute error') plt.ylabel('mean absolute error') plt.xlabel('epoch') plt.legend(['train', 'validation'], loc='upper left') plt.show()
The preceding code uses validation_split=0.2 to keep 20% of the data outofsample to test the model at each epoch.
[ 69 ]
Designing Neural Network Architectures
Chapter 2
The model is further scored against the test set we created before. The result of plotting is shown in Figure 9, where you can see how MAE decreases with the 100 epochs used for training. The trend is almost similar, with the error on 80% of the training set used for training lower than the error on the remaining 20% used for testing. It is interesting to note that while the error on the training data tends to decrease anyway, there is a point where the test error does not decrease. This point separates the network underfitting from the network overfitting:
Figure 10: MAE with respect to training set and validation set along the epochs
When training a model, note the following: One important step consists of considering different options regarding the structure, such as the number of units at the hidden layer or even the number of layers, the activation functions, and several parameters controlling the learning process. To identify them clearly, it is common to refer to them as hyperparameters. Since these parameters are fixed for a given training session, we need to tune them by comparing the models obtained at the end of the training sessions.
[ 70 ]
Designing Neural Network Architectures
Chapter 2
This requires us to use a portion of data to choose the best model. This subset is called a validation set. However, when there is no need to optimize the hyperparameters, there is no distinction between the validation and test set, and the two terms are used interchangeably.
Neurons based on distance Conventional neurons used in MLP are functions whose value depends on the distance of input from the hyperplane given by the neuron. We can design neurons, whose value depends only on the distance, , between the input, , and some fixed point, . The output fired by such a neuron is as follows:
Here, the activation function,
Here, the
, is generally the Gaussian function:
constant is the shape hyperparameter. Other common choices are as follows: Multiquadratic: Inverse quadratic:
Inverse multiquadratic:
We reformulate the classification problem outlined in the previous section. In Keras, there is no native support for RBF; hence, we have to define a custom Layer to implement them. Defining custom Layer is very helpful in all of those circumstances we need to implement a custom function or operation. To achieve this, you need to implement a few methods: __init__: The constructor where we provide specific hyperparameters for the
layer build: Where the trainable parameters are defined
[ 71 ]
Designing Neural Network Architectures
Chapter 2
call: In charge of computing the output tensor given the input tensor compute_output_shape: Returns the output tensor size
The gradient is computed automatically. In the case of RBF, the implementation may look like the following: class RBF(Layer): def __init__(self, units, gamma, **kwargs): super(RBF, self).__init__(**kwargs) self.units = units self.gamma = K.cast_to_floatx(gamma) def build(self, input_shape): self.mu = self.add_weight(name='mu', shape=(int(input_shape[1]), self.units), initializer='uniform', trainable=True) super(RBF, self).build(input_shape) def call(self, inputs): diff = K.expand_dims(inputs)  self.mu l2 = K.sum(K.pow(diff, 2), axis=1) res = K.exp(1 * self.gamma * l2) return res def compute_output_shape(self, input_shape): return (input_shape[0], self.units)
Once the RBF layer is defined, it can be used to build a model accordingly. Then, the model is trained and scored: # constructing a (simple) RBF neural network model = Sequential() input = Input(shape=(X_train.shape[1],)) rbf=RBF(10, 0.5)(input) out=Dense(1, activation='sigmoid')(rbf) model = Model(inputs=input, outputs=out) model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy']) model.summary()
[ 72 ]
Designing Neural Network Architectures
Chapter 2
The model is then specified and compiled, and we can train and score it as usual: # training the network model.fit(X_train, y_train, epochs=200, verbose=0) # scoring it on the data it trained on as well as test data scores = model.evaluate(X_train, y_train) print ("Training Accuracy: %.2f%%\n" % (scores[1]*100)) scores = model.evaluate(X_test, y_test) print ("Testing Accuracy: %.2f%%\n" % (scores[1]*100))
Obviously, it is possible to use an RBF layer within more complex architectures, stacking multiple RBF and dense layers or even within convolutional and recurring architectures, which we will talk about later.
Computing with tensors Nowadays, it is very common to speak of tensors when referring to the processing produced by a neural network. Informally, a tensor is a multidimensional matrix. It can be defined as an array of numbers arranged on a regular grid with a variable number of axes. In practice, it is a list of values to which a coordinate system is associated. The order of a tensor defines the dimensionality of the matrix or, in other terms, the number of axes. Therefore, a scalar is represented as a tensor of order 0, a vector as a tensor of order 1, an ordinary matrix as a tensor of order 2, and an dimensional matrix as a tensor of order . In a similar way to matrices, a tensor is represented by a capital letter. For example, with we can indicate the set of weights of a fully connected layer, where the element is the weight associated to input for the unit . The use of tensors allows us to represent in an extremely compact way the operations carried out by the single layers. For example, if it represents the (column) input vector, the potentials are . To calculate the response of the layer, just consider . A tensor can be easily defined using NumPy's ndarray. The following example shows how to create a tensor of order 3: from numpy import array # create tensor U = array([ [[111,112,113], [121,122,123], [131,132,133]], [[211,212,213], [221,222,223], [231,232,233]], [[311,312,313], [321,322,323], [331,332,333]], ])
[ 73 ]
Designing Neural Network Architectures
Chapter 2
print(T.shape) print(T)
By extending the operations between matrices, it is possible to obtain the elementwise sum, subtraction, multiplication, and division of tensors: V = array([ [[411,412,413], [421,422,423], [431,432,433]], [[511,512,513], [521,522,523], [531,532,533]], [[611,612,613], [621,622,623], [631,632,633]], ])
A = U + V print(A) S = U  V print(S) M = U * V print(M) D = U / V print(D)
To make the tensor product, we can use the tensordot function, which requires you to specify the axis to be used for the usual row/column product of matrices: from numpy import tensordot P = tensordot(U, V, axes=0) print(P)
A very useful operation is reshape. With it, we can make a tensor compatible to be processed by a layer. For example, if our input to a network is represented by a data matrix and we need to get a vector, we can easily make the input data flat as shown in the following: from numpy import reshape T = array([[1,2],[3,4],[5,6]]) X = reshape(T,[6,1]) print(X)
[ 74 ]
Designing Neural Network Architectures
Chapter 2
Tensors can be useful to calculate the output of a network applied to an entire batch of data. Suppose, for example, that our model is represented by an MLP network of two levels. Let be the batch of data supplied as input to the network, consisting of samples, of components each, placed in the column. So, tensor will be a matrix composed of rows and columns. We simply will have the following:
Here, and are respectively the weights of layers 1 and 2 and That is amazingly compact!
and
, the biases.
Training a network through backpropagation The output of a neural network depends on the weights used. In an attempt to make the output closer to the desired target, we can minimize the error in the output of the neural network through gradient descent. In other terms, we perform an iterated correction of weights according to the following rule:
Here, is the error cost function, is the current iteration, and is the learning rate. For the sake of simplicity, we will omit the explicit reference to the iteration in the rest of this chapter. Also, we will refer to the whole space of parameters using the gradient notation:
There are different formulations of the error cost function. Popular choices in Keras have been outlined in Table 2. Whatever the error cost function, , is, we have the following:
[ 75 ]
Designing Neural Network Architectures
Chapter 2
Here, is the gradient of with respect to the network output, , which in turn depends on the weights, . The relationship between the gradient and weights depends on the layer we are considering. To understand this, let's suppose that our network is an MLP made of only two layers: inputs are given to the first layer and then the output of this (hidden) layer is given to a second (output) layer. For the sake of simplicity, we omit the biases. We will also consider one single sample provided as input to the network. In that case, we have this:
In this case, we need to make a distinction between the first layer weights, second layer weights, . Let's start from the latter:
, and the
If the activation is a sigmoid, we have the following:
The computation for the weights attaining the first layer is different. In this case, we have the following:
More compactly, we can write this:
The preceding assumes the following:
[ 76 ]
Designing Neural Network Architectures
Chapter 2
Computing it is not useful to correct weights, but it helps us to generalize the algorithm when multiple layers are stacked together, as shown in Figure 11. We can represent that architecture in terms of blocks, as shown in the following:
Figure 11: Backpropagation
In this case, the kth layer is trained according to the equation:
However, the correction is propagated back by the following equation:
If the kth block performs something different than a perceptron, we can adapt the backpropagation to accomodate a different functional relationship. Also, the preceding equations can be applied to networks whose topology is more complex than multilayer perceptron. This makes it possible to extend the application of backpropagation to deep learning. If you are interested in fully understanding backpropagation, a nice example is provided by Matt Mazure's blog at https://mattmazur.com/2015/03/17/astepbystepbackpropagationexample/.
[ 77 ]
Designing Neural Network Architectures
Chapter 2
Understanding CNNs CNNs represent one of the most wellknown deep architectures. They are specifically designed to process 2D data, such as images, charts, and matrices, but can also be used for 1D, such as time series or sequences, or 3D data, such as a point cloud (non financial) or timevariant geolocalized economic data. An MLP architecture like the one shown in Figure 9 has the characteristic that the input is supplied in its totality to all of the units of the first hidden layer, and the output of these is supplied in input to all of the units of the next layer and so on. Therefore, an MLP achieves global pattern recognition. Recognizing complex patterns in this way can be arduous. In fact, this would require finding out how the values of the input variables relate to each other in determining the output of the network. In local pattern recognition, a limited portion of the input is used, called the local receptive field, which is supplied to a unit to determine the match with a desired pattern. For example, suppose we are interested in determining the presence of the pattern 1, 1, + 1, + 1, + 1 in variations within a price sequence. To do this, we can consider the scalar product, , where w keeps the pattern we are interested in and x the sequence of variations to be analyzed. The obtained value will be maximum (equal to 5) when there is a perfect correspondence between the pattern of our interest and the sequence of variations, that is, when the price variations, x, will be exactly equal to 1, 1, + 1, + 1, +1. When instead the variations will be opposite to the pattern, that is, equal to +1, +1, 1, 1, 1, the product, , will be equal to 5, or the minimum. This correspondence can, therefore, be realized by a perceptron with Heaviside step as the activation function, whose output threshold value will decide when this correspondence will be identified. If this threshold value is equal to 5, the perceptron will output 1 only in the presence of a perfect match. If the threshold value is equal to 3, this correspondence is achieved in 4 points out of 5, and so on. It is possible to use other options for the activation function. If we use a sigmoid, this value will increase from 0 to +1 in relation to the number of matches with the pattern identified in the sequence. If we use a ReLU, what we get is the number of matches when they are greater than the mismatches; otherwise, we get 0.
[ 78 ]
Designing Neural Network Architectures
Chapter 2
If this research is extended to the whole sequence, it is possible to determine where and to what extent we correspond to the pattern we are interested in, as shown in Figure 12:
Figure 12: Sliding a pattern looking for matches along the input
The name of the net is given by the presence of a convolutional layer: it carries out the operation of convolution. Convolution is not a new concept for those who apply signal processing methods to finance, and trading in particular. However, in the context of neural networks, convolution assumes a specific meaning in pattern recognition. In fact, as convolution is an operation aimed at promoting the multiplication of a set of weights called kernel (or filter) with the input to the movement of a window along all the dimensions of the data, it realizes local patternmatching through a complete scan of the input data. The motions of the same filter (set of weights) for the input matrix occur at different points of the input. In other words, the same pattern is searched within all input data through the systematic application of the filter to each overlapping part of the input data, from left to right, from top to bottom.
[ 79 ]
Designing Neural Network Architectures
Chapter 2
This is a powerful idea: the systematic application allows you to discover a pattern within the data wherever it is located. This property is called translation invariance. The translation of the kernel can take place from element to element, or with wider variations. This hyperparameter is called a stride. By default, the stride is worth 1, but there are also common strides of 2 or more. It is important to note that, in practice, the kernel is not translated, but the same pattern (weight sharing) is matched against different portions of the input (local receptive field), as shown in Figure 13:
Figure 13: CNN basic operations and the related hyperparameters
The matching result is recorded in a corresponding plan called a feature map: the application of a kernel produces a corresponding feature map. The kernel has a fixed size. Typical values are 3, 5, and 7, although larger quantities are possible. The reason for assuming an odd value lies in the possibility of using, as a kernel reference, a single central element whose coordinates are attributed to the pattern recognition in the feature map.
[ 80 ]
Designing Neural Network Architectures
Chapter 2
A convolutional layer refers to multiple filters, whose application of each produces a corresponding future map. The hyperparameter used to tell the number of filters assigned to a convolutional layer is named depth. The pointwise sum of maps represents the input given to the next layer. Generally, between two convolutional layers, an aggregation layer is placed to compress the information. This operation is named pooling as it is meant to extract meaningful information by a rolling window over data. In the past, pooling was performed by averaging data within the window, but nowadays maximum is the preferred option for pooling. The result of pooling consists of downsampling the available information so that the presence of any pattern is highlighted at a lower resolution. The reduced input size allows us to consider a larger number of filters because of the memory available to store more feature maps and the reduced time required to process the input. Because of this, the number of filters tends to increase when the level rises. This is also compatible with the need to detect more complex patterns when increasing levels, while at lower levels, in general, there are fewer useful patterns to consider. For example, in the case of images, at lower levels, it is a matter of finding elementary patterns such as segments, angles, and simple shapes. As you level up, these patterns aggregate into increasingly complex shapes that require more convolution filters to be effectively detected.
LeNet5, AlexNet, and others LeNet5 was the first prominent example of a CNN. It is the result of the pioneering work done by Yan LeCun in the 90s. LeNet5 is made of 7 layers, as depicted in Figure 14:
Figure 14: Diagram of LeNet5
[ 81 ]
Designing Neural Network Architectures
Chapter 2
The input is given as 32 x 32 black and white images representing handwritten or printed digits. The first layer performs a convolution using a 5 x 5 kernel with stride 1 and valid padding over 6 feature maps. Therefore, each feature map of the first layer is 28 x 28. The second layer (pooling) produces a subsampling of each feature map, by averaging pixels using a 2 x 2 rolling window with a stride of 2. The result is 14 x 14 x 6 feature maps. The third layer performs a further convolution with a 5 x 5 kernel, stride 1, valid padding, which ends up in 10 x 10 x 6 feature maps. Then, we get again a pooling with a 2 x 2 window and stride 2, resulting in a 16 x 5 x 5 feature plane. A final convolutional layer with a 5 x 5 kernel, stride 1, and valid padding, produces 120 x 1 x 1 feature maps that are flattened and given as input to a fully connected layer made of 84 units, and finally to a 10unit output layer. A possible implementation of LeNet5 in Keras is given as follows: from keras import Sequential from keras.layers import Dense, Conv2D, Flatten, MaxPooling2D # Instantiate an empty model model = Sequential() # C1 Convolutional Layer model.add(Conv2D(filters=6, kernel_size=5, strides=1, activation='relu', input_shape=(32, 32, 1), paddind="valid")) # S1 Pooling Layer model.add(MaxPooling2D(pool_size=2, strides=2)) # C2 Convolutional Layer model.add(Conv2D(filters=16, kernel_size=5, strides=1, activation='relu', input_shape=(14, 14, 6), paddind="valid")) # S2 Pooling Layer model.add(MaxPooling2D(pool_size=2, strides=2)) # Flatten the output to feed the fully connected layers model.add(Flatten()) # FC1 Fully Connected Layer model.add(Dense(units=120, activation='relu')) # FC2 Fully Connected Layer model.add(Dense(units=84, activation='relu')) # FC3 Fully Connected Layer model.add(Dense(units=10, activation='softmax'))
[ 82 ]
Designing Neural Network Architectures
Chapter 2
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) model.summary()
The padding parameter used in Conv2D aims at specifying the method used to deal with the borders of input/feature planes. It gets three possible values: 'valid': With no padding, the resulting output is smaller than the input
(default). 'causal': It expands the plane by filling the borders with zeros, and the output has the same shape as the input. 'same': It expands the plane by filling the borders with input values, and the output has the same shape as the input. From the summary, we can see how the number of parameters of LeNet 5 is much larger than the networks we have considered so far: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_1 (Conv2D) (None, 28, 28, 6) 156 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 14, 14, 6) 0 _________________________________________________________________ conv2d_2 (Conv2D) (None, 10, 10, 16) 2416 _________________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 5, 5, 16) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 400) 0 _________________________________________________________________ dense_1 (Dense) (None, 120) 48120 _________________________________________________________________ dense_2 (Dense) (None, 84) 10164 _________________________________________________________________ dense_3 (Dense) (None, 10) 850 ================================================================= Total params: 61,706 Trainable params: 61,706 Nontrainable params: 0 _________________________________________________________________
[ 83 ]
Designing Neural Network Architectures
Chapter 2
The complexity of LeNet was due to the hardware capabilities of that time, before the advent of CUDA and GPU computing. In 2011, Alex Krizhevsky, at that time a Ph.D. student at the University of Toronto under the supervision of Geoffrey Hinton, proposed a larger CNN to face the ImageNet Large Scale Visual Recognition Challenge (ILSVRC). Figure 15 outlines the architecture:
Figure 15: Diagram of AlexNet
The ILSVRC was a competition aimed at benchmarking algorithms for object detection and image classification at a large scale, on hundreds of object categories and millions of images. The challenge was launched in 2010 and run annually, with participants coming worldwide from more than 50 institutions. During the last ILSVRC in 2017, 29 out of 38 teams reached an accuracy of over 95%. For this reason, ImageNet ILSVRC evolved toward more difficult tasks.
[ 84 ]
Designing Neural Network Architectures
Chapter 2
There are several novelties in AlexNet. The first consists of using 227 x 227 RGB color images. The first layer is convolutional, made of 11 x 11 kernels, stride 4, valid padding, which leads to having 55 x 55 x 96 feature maps as output. It is followed by a maxpooling stage implemented by 3 x 3 kernel, stride 2, which reduces the volume to 27 x 27 x 96. A third layer performs a 5 x 5 kernel (stride 1 and the "same" padding) convolution that ends up as a 27 x 27 x 256 feature maps, followed by a 3 x 3 stride 2 maxpooling layer that leads to having 13 x 13 x 256 feature maps. Then, we have three convolutions in sequence using a 3 x 3 kernel, stride 1, and the "same" padding, leading respectively to 13 x 13 x 384, 13 x 13 x 384, and 13 x 13 x 256 feature maps. They are followed by a 3 x 3, stride 2 maxpooling that leads to having 6 x 6 x 256 feature maps. They are flattened and given to four fully connected layers made respectively of 9,216, 4,096, 4,096, and 1,000 units. A code implementing AlexNet in Keras is shown as follows. As usual, first of all, we import the elements of Keras useful to our model and instantiate a Sequential object: from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D, BatchNormalization model = Sequential()
Then, we create the first convolutional layer, made of 96 filters, with a kernel size of 11 x 11 and stride 4, and the ReLU activation function. MaxPooling2D follows to subsample the outcome of the Conv2D layer, assuming pool_size=(2,2) and strides=(2,2): # 1st Convolutional Layer model.add(Conv2D(filters=96, activation='relu',input_shape=(224, 224, 3), kernel_size=(11, 11), strides=(4, 4))) # Pooling model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # Batch Normalisation before passing it to the next layer model.add(BatchNormalization())
In the code, we made use of a new component: BatchNormalization. The purpose of batch normalization is to rescale values to get 0 mean and 1 standard deviation. This allows the hidden layers to avoid saturation levels that are too hard to revert. The benefits are multiple: The values stand in a range where units are most dynamic. We can use higher learning rates because no activation goes too high or too low. It is easier to retrain a network with new data.
[ 85 ]
Designing Neural Network Architectures
Chapter 2
It makes the layers less dependent on each other. It reduces overfitting and covariance shift. There's lower Dropout. The first stage is followed by another stage with a very similar scheme (Conv2D + MaxPooling2D + BatchNormalization): # 2nd Convolutional Layer model.add(Conv2D(filters=256, activation='relu', kernel_size=(11, 11), strides=(1, 1))) # Pooling model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # Batch Normalisation model.add(BatchNormalization())
Two stages consisting of Conv2D + BatchNormalization follow. According to Figure 15, no MaxPooling2D is required: # 3rd Convolutional Layer model.add(Conv2D(filters=384, activation='relu', kernel_size=(3, 3), strides=(1, 1))) # Batch Normalisation model.add(BatchNormalization()) # 4th Convolutional Layer model.add(Conv2D(filters=384, activation='relu', kernel_size=(3, 3), strides=(1, 1))) # Batch Normalisation model.add(BatchNormalization())
A further stage made of Conv2D + MaxPooling2D + BatchNormalization closes the sequence of convolutional layers: # 5th Convolutional Layer model.add(Conv2D(filters=256, activation='relu', kernel_size=(3, 3), strides=(1, 1))) # Pooling model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # Batch Normalisation model.add(BatchNormalization())
[ 86 ]
Designing Neural Network Architectures
Chapter 2
To make the output of the convolutional component available as input to the fully connected AlexNet component, we need to transform it through Flatten: # Passing it to a dense layer model.add(Flatten())
Then, we have three Dense layers with the ReLU activation function. Each layer is followed by Dropout and BatchNormalization: # 1st Dense Layer model.add(Dense(4096, activation='relu', input_shape=(224*224*3,))) # Add Dropout to prevent overfitting model.add(Dropout(0.4)) # Batch Normalisation model.add(BatchNormalization()) # 2nd Dense Layer model.add(Dense(4096, activation='relu')) # Add Dropout model.add(Dropout(0.4)) # Batch Normalisation model.add(BatchNormalization()) # 3rd Dense Layer model.add(Dense(1000, activation='relu')) # Add Dropout model.add(Dropout(0.4)) # Batch Normalisation model.add(BatchNormalization())
Finally, a Dense layer with softmax as the activation function is used to infer the final prediction regarding the class belonging to the corresponding input: # Output Layer model.add(Dense(17, activation='softmax'))
Once the model is defined, we can use summary to outline the network structure and characteristics and compile it: model.summary() model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
[ 87 ]
Designing Neural Network Architectures
Chapter 2
AlexNet was a milestone in deep learning since it improved +10.8% in accuracy, with respect to the runner up of ILSVRC at that time, in 2012. The following is the model summary that reflects Figure 19: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_1 (Conv2D) (None, 54, 54, 96) 34944 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 27, 27, 96) 0 _________________________________________________________________ batch_normalization_1 (Batch (None, 27, 27, 96) 384 _________________________________________________________________ conv2d_2 (Conv2D) (None, 17, 17, 256) 2973952 _________________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 8, 8, 256) 0 _________________________________________________________________ batch_normalization_2 (Batch (None, 8, 8, 256) 1024 _________________________________________________________________ conv2d_3 (Conv2D) (None, 6, 6, 384) 885120 _________________________________________________________________ batch_normalization_3 (Batch (None, 6, 6, 384) 1536 _________________________________________________________________ conv2d_4 (Conv2D) (None, 4, 4, 384) 1327488 _________________________________________________________________ batch_normalization_4 (Batch (None, 4, 4, 384) 1536 _________________________________________________________________ conv2d_5 (Conv2D) (None, 2, 2, 256) 884992 _________________________________________________________________ max_pooling2d_3 (MaxPooling2 (None, 1, 1, 256) 0 _________________________________________________________________ batch_normalization_5 (Batch (None, 1, 1, 256) 1024 _________________________________________________________________ flatten_1 (Flatten) (None, 256) 0 _________________________________________________________________ dense_1 (Dense) (None, 4096) 1052672 _________________________________________________________________ dropout_1 (Dropout) (None, 4096) 0 _________________________________________________________________ batch_normalization_6 (Batch (None, 4096) 16384 _________________________________________________________________ dense_2 (Dense) (None, 4096) 16781312 _________________________________________________________________ dropout_2 (Dropout) (None, 4096) 0 _________________________________________________________________ batch_normalization_7 (Batch (None, 4096) 16384 _________________________________________________________________ dense_3 (Dense) (None, 1000) 4097000
[ 88 ]
Designing Neural Network Architectures
Chapter 2
_________________________________________________________________ dropout_3 (Dropout) (None, 1000) 0 _________________________________________________________________ batch_normalization_8 (Batch (None, 1000) 4000 _________________________________________________________________ dense_4 (Dense) (None, 17) 17017 ================================================================= Total params: 28,096,769 Trainable params: 28,075,633 Nontrainable params: 21,136 _________________________________________________________________
From that moment, several deep learning architectures have been proposed. Among the others, it is worth mentioning GoogLeNet. The major novelty of GoogLeNet was to consider different alternatives in a module called Inception that holds convolutions and maxpooling windows of different shapes. This allows the network to look at data wider rather than deeper with different levels of details, letting the network learn what is the most appropriate to be used in a specific context. Figure 16 outlines the internal structure of the Inception module:
Figure 16: The Inception module
[ 89 ]
Designing Neural Network Architectures
Chapter 2
Following the internal architecture given in the preceding diagram, here is an example of how to implement the Inception module in Keras: inception_1x1 = Conv2D(filters=64, kernel_size=(1,1), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(input) inception_3x3_reduce = Conv2D(filters=96, kernel_size=(1,1), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(input) inception_3x3_conv = Conv2D(filters=128, kernel_size=(3,3), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(inception_3x3_reduce) inception_5x5_reduce = Conv2D(filters=16, kernel_size=(1,1), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(input) inception_5x5_conv = Conv2D(filters=32, kernel_size=(5,5), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(inception_5x5_reduce) inception_3x3_pool = MaxPooling2D(pool_size=(3,3), strides=(1,1), padding='same')(input) inception_3x3_pool_proj = Conv2D(filters=32, kernel_size=(1,1), padding='same', activation='relu', kernel_regularizer=l2(0.0002))(inception_3x3_pool) inception_output = Concatenate(axis=1, name='inception_output')([inception_1x1_conv,inception_3x3_conv,inception_5 x5_conv,inception_3x3_pool_proj])
Based on the Inception module, GoogLeNet has a structure as outlined in Figure 17:
Figure 17: Architecture of GoogLeNet
[ 90 ]
Designing Neural Network Architectures
Chapter 2
GoogLeNet is one of the architectures already available in Keras, with trained parameters. The following table summarizes them (source: Keras documentation): Model (Class Name)
Size Top1 Accuracy Top5 Accuracy Parameters Depth
Xception 88 MB 0.790 VGG16 528 MB 0.713 VGG19 549 MB 0.713 ResNet50 98 MB 0.749 ResNet101 171 MB 0.764 ResNet152 232 MB 0.766 ResNet50V2 98 MB 0.760 ResNet101V2 171 MB 0.772 ResNet152V2 232 MB 0.780 InceptionV3 92 MB 0.779 InceptionResNetV2 215 MB 0.803 MobileNet 16 MB 0.704 MobileNetV2 14 MB 0.713 DenseNet121 33 MB 0.750 DenseNet169 57 MB 0.762 DenseNet201 80 MB 0.773 NASNetMobile 23 MB 0.744 NASNetLarge 343 MB 0.825
0.945
22,910,480
126
0.901
138,357,544 23
0.900
143,667,240 26
0.921
25,636,712

0.928
44,707,176

0.931
60,419,944

0.930
25,613,800

0.938
44,675,560

0.942
60,380,648

0.937
23,851,784
159
0.953
55,873,736
572
0.895
4,253,864
88
0.901
3,538,984
88
0.923
8,062,504
121
0.932
14,307,880
169
0.936
20,242,984
201
0.919
5,326,716

0.960
88,949,818

Here it is how to load a pretrained model in Keras: from keras.applications.inception_v3 import InceptionV3 from keras.layers import Input model = InceptionV3(input_tensor=Input(shape=(224, 224, 3)) , weights='imagenet', include_top=True) model.summary
GoogLeNet is much wider than the previous ones, as highlighted by the following code: ========================================================================= Total params: 23,851,784 Trainable params: 23,817,352 Nontrainable params: 34,432 ______________________________________________________________________
[ 91 ]
Designing Neural Network Architectures
Chapter 2
In this, as in the previous example, we can see the presence of nontrainable parameters: They come from BatchNormalization, whose mean and variance vectors are updated trough layer.add_updates according to the layer's input, instead of backpropagation. For this reason, they are named nontrainable. They are not hyperparameters.
Understanding RNNs The networks discussed so far are feedforward. That is, they transform an input into a desired output using a series of mathematical operations, crossing a series of levels whether they are fully connected, convolutional, pooling, or something else. Each input is independent of those that have preceded it. The operations are repeated without keeping memory of what was previously fed to the network. Therefore, a feedforward network has no idea of sequences in time. Recurring networks were introduced to meet the need to determine the response not only based on the current input but also on the context in which that input occurs. This possibility is of particular interest in the world of finance where the data to be analyzed is inherently timerelated. To do this, we need the network to keep the memory of what has already been observed. This is done by introducing the concept of state, as highlighted in Figure 18. This scheme is known as an RNN:
Figure 18: General structure of RNNs
[ 92 ]
Designing Neural Network Architectures
Chapter 2
Over time, different declensions of the preceding scheme have been proposed. The reference schemes used for a long time are known as the Jordan network (1986) and Elman network (1990). Their structures are shown in Figure 19:
Figure 19: The Jordan network and the Elman network
Both networks implement the state through a context that is kept by a registry unit to reproduce the value at their entry at the next instant. The main difference is that Jordan reproposes the network output at the previous instant, while Elman reproposes the value of the hidden units. This feature has made Elman's solution long preferred to the Jordan network. However, both suffer from a known problem of recurring networks: the vanishing (and exploding) gradient. To understand the reasons for this, we have to decline the training of the network over time. The training algorithm is known as Backpropagation Through Time (BPTT) and it is based on the "unrolling" (also called the "unfolding") of the RNN. To understand how it works, for simplicity, let's consider an elementary network composed of only one hidden unit and one output unit, as shown in Figure 20:
Figure 20: Unrolling of an RNN
[ 93 ]
Designing Neural Network Architectures
Chapter 2
Once the network is unrolled, its structure becomes similar to a multilayer feedforward network, and we can use backpropagation as a means to update the weights and so train the network. The BPTT algorithm is outlined in Figure 21:
Figure 21: BPTT
As we can see, the weight, follows:
,
is updated according to a variation that is computed as
[ 94 ]
Designing Neural Network Architectures
Chapter 2
Here, we have the following: As usual, represents the error, the weight, the firstorder derivative of the output, , and the hidden layer output at time . Therefore, the correction, , is only determined by the value of at time , modulated by the derivative, . Otherwise, updating the weights, and , also takes into account the error propagation due to past inputs. Indeed, we get the following:
The problem of the vanishing (exploding) gradient arises by considering that, given , we have the following:
Therefore, the further we move away over time, the greater the impact of the product becomes. If we use the sigmoid, its derivative is less than 1 all along the axis. For example, in the case of the standard logistic function, the maximum value of 0.25 is reached in 0. This means that we have the following:
[ 95 ]
Designing Neural Network Architectures
Chapter 2
So, the contribution made by the more distant values is strongly attenuated by the multiplication of derivatives: In case the gradient should be greater than 1, as you can understand with faster switching functions, the phenomenon becomes amplification. In this case, we speak of an exploding gradient. This problem has greatly limited the use of Elman (and Jordan) recurring networks in the past. In fact, these networks had the limitation of not being able to learn correctly the impact that events far away in time can have on the current response of the network. This is an important aspect to take into account in many applications, including finance. To overcome the problem of vanishing (exploding) gradients, it is necessary to interrupt the multiplicative effect of the gradient. This is what an LSTM cell and Gated Recurrent Unit (GRU) do.
Long ShortTerm Memory (LSTM) LSTM is an architecture initially proposed by Sepp Hochreiter and Juergen Schmidhuber in 1997. Their structure includes a cell in the center, shown in Figure 20, to which the input, , is provided at each instant of time, together with the output, , and the context, , of the previous instant. The internal logic of the cell can be trained to determine the output, , at the current instant and the context, , to be used for the next instant, according to these equations:
Here, we assume that
and
.
[ 96 ]
Designing Neural Network Architectures
Chapter 2
The internal structure of an LSTM cell is outlined in Figure 22:
Figure 22: LongShort Term Memory (LSTM)
The preceding structure illustrates the following: The , , and vectors are used to control the respective forget, input, and output ports. In practice, through the operator, , which denotes the Hadamard product (elementwise product), some values are deleted or allowed to pass. The activation logic is learned through a perceptron layer with activation given by the standard logistic function, , while the activation function used to determine the output and context is the hyperbolic tangent that provides values in the range of [1,1]. This feature is important for cell feedback. It is also important to note that all of the quantities involved are vectors. So, a different pass/delete logic of each gate applies individually to each line. The internal structure of an LSTM cell allows each stage to be decoupled. The context is reenacted at each step, allowing you to decide which information to let pass unchanged through the cell and which to delete/modify. This feature makes it possible to keep some memory lines unaltered over long periods of time, while others have a much shorter life. Which memory to erase and which to set depends on the state of the cell and the current input. Context and input, appropriately selected by the logic of the respective ones, are used to determine the network output.
[ 97 ]
Designing Neural Network Architectures
Chapter 2
Gated recurrent unit The articulated internal logic of an LSTM cell complicates training. In an attempt to solve the problem, in 2014, Kyunghyun Cho, at that moment a member of Yoshua Bengio's Mila lab, proposed the GRU, a simplified LSTM cell, the scheme of which is shown in Figure 23:
Figure 23: GRU
Basically, it is an LSTM cell without the output gate. Equations governing the cell's internal logic are the following:
Here, we have . It is possible to further simplify the structure of a cell. For example, you can simplify the activation of and in the following types, by removing part of the contributions: Type 1
Type 2
[ 98 ]
Type 3
Designing Neural Network Architectures
Chapter 2
Figure 24 compares the different logic followed by Elman, LSTM, and GRU cells:
Figure 24: Elman, LSTM, and GRU compared
In essence, the LSTM and GRU solution, compared to Elman's initial scheme, add active feedback lines, which allow, on the one hand, reinforcing/deleting the memory locally at that instant of time and isolating its behavior from the previous instants.
Summary In this chapter, we entered the world of deep neural networks, analyzing their functionalities, structures, architectures, and training. We started from the elementary unit at the base of the networks: the neuron. We then moved on to organized structures of neurons, starting from the simple row arrangement of a layer to convolutional networks. We saw how a network is trained using the descending gradient principle implemented by the backpropagation algorithm. Then, we introduced convolution and how it has been used in remarkable architectures such as LeNet, AlexNet, and others. Finally, we introduced recurring neural networks, elaborating on the problems of the first schemes and coming to the most recent schemes of LSTM and GRU. In the next chapter, we will see how to use neural networks to build models and how they are validated and optimized.
[ 99 ]
3 Constructing, Testing, and Validating Models In the previous chapter, we learned the principles of functionality and the basis of neural networks. We also saw some complex architectural patterns. However, whatever solution is chosen to analyze the data, it is necessary that the model that's been developed becomes part of a broader view. In this chapter, we will look at the life cycle of a model in finance, starting from the identification of a profitable opportunity and how to translate it into an operational model based on machine learning, through to its prototyping and then its deployment and benchmarking to its retirement. The following topics will be covered in this chapter: Building blocks of financial models Formulating a hypothesis for trading Selecting relevant financial models Acquiring data from multiple sources Implementing the mode Evaluating an investment strategy Tuning the model Going live Benchmarking
Constructing, Testing, and Validating Models
Chapter 3
Building blocks of financial models A financial model captures the underlying structure of an asset class to predict future returns and/or to figure out risk characteristics. The three key aspects of building financial models are as follows: Hypothesis about tradable market inefficiency Acquiring relevant data and scrubbing it in a proper format Developing a relevant model to exploit in order to test the hypothesis The subsequent chapters of this book deal with all of these aspects in a lot of detail for specific problem examples. In this chapter, we will look at the process of developing a model in its entirety, from the identification of a trading opportunity to its disposal. The development and delivery of an ML model in finance is a complex activity that encompasses multiple activities and phases, as depicted in the following diagram:
Activities that make up the life cycle of an ML model in ﬁnance
[ 101 ]
Constructing, Testing, and Validating Models
Chapter 3
The process is structured as follows: Inception is offered by a trading idea or an opportunity that is seen in the markets. This leads to a trading hypothesis from which we can build a quantitative model that will be used to guide investment decisions. The model, therefore, in its definition, must respond adequately to the hypothesis formulated and once finished, it will have to be able to validate it, before proceeding to its real deployment and operation. The definition of the model determines the need for data and its acquisition, which generally precedes the subsequent phases of coding, training, testing, and optimization. The data is divided and organized to provide a training set for learning the parameters of the model (and sometimes the structure), a validation set for its evaluation and measurement of performance, and a test set to evaluate the final solution that will become operational. Once the data has been defined, the model is coded using supporting frameworks and tools, including Keras. During its development, the model is tested and evaluated quantitatively using appropriate metrics. The results of this phase may lead to rethinking the model and redesigning it. When we are satisfied with the result, the model can be optimized by looking at the different options we have available in terms of hyperparameters and choosing the best configuration. This step concludes the prototyping phase of the model. During this last step, it may be necessary to intervene with the implementation or even with the initial definition of the model if the results are not satisfactory. If, on the other hand, the model result is satisfactory, it can be deployed and made operational. This phase is, in itself, articulated and complex, with further phases of coding, training, and validation, but for the purposes of this book, it will not be explored in detail. Since market conditions may change, it is necessary to monitor the model's performance to record when it deviates from the expected behavior. In this case, the model, which is no longer suitable, must be rethought. This may require retraining the model through a new tuning and selection activity. In some cases, the model needs to be significantly rethought in terms of its definition. Finally, it may be necessary to question the hypothesis on which it is based and the assumptions about the market itself. In the remainder of this chapter, we will discuss each of the activities in more detail.
[ 102 ]
Constructing, Testing, and Validating Models
Chapter 3
Formulating a hypothesis for trading The critical element in the formulation of an investment strategy is the basic idea about an inefficiency that can be exploited. As noted in Chapter 1, Deep Learning for Finance 101, a fundamental concept in finance is the Efficient Market Hypothesis (EMH), formulated by Fama in the 1970s. According to this concept, the price of an asset incorporates all of the information available at the time. In an efficient market, it is difficult to generate profits from trading activities, since the perfect knowledge of all players does not allow them to gain an advantage over others. However, it is difficult for a market to be perfectly efficient because of the lack of perfect knowledge on the part of all players. Each market player, in conducting their trading operations, will use a portion of the information that reflects a set of observations, considerations, and projections about the future. So, the intersection between supply and demand in a market is a consequence of the intersection between different market perceptions. Trends in prices, traded volumes, and even transactions are a reflection of this. Developing models in quantitative finance means analyzing market data to discover market inefficiencies, extract the information content from them, and take advantage of it. To achieve all of this, we need models that can conduct this activity systemically. AI and modern machine learning are very promising in this direction, as they allow us to operate on large amounts of data. Such data is a representation of a single market or multiple markets. Advanced analytics based on AI looks for recurring patterns that can give us a useful advantage to understand the future value of a position taken on the market. However, it is not possible to construct a model without having intuited a possible inefficiency to exploit. But you must know that not all market inefficiencies are negotiable. They are negotiable when three conditions are met: Inefficiency can be easily translated into structured or vanilla financial products: For example, if you have a principal components analysis on the residual of a time series after detrending results in a significant factor, but this does not map to any existing set financial instrument, the inefficiency can exist but is not tradable. Financial instruments are accessible: These could be customized or off the shelf, but there needs to be a counterpart to take the opposite side of the trade. Housing market inefficiency was not tradable until derivatives were structured and marketed by investment banks. Infrastructure continues to remain an inefficient market that cannot be traded due to the monopolistic nature of assets and the regulatory framework.
[ 103 ]
Constructing, Testing, and Validating Models
Chapter 3
Transaction costs are not prohibitive: Taking a short position in a closely held company is very likely to result in profits being eroded by a short squeeze. The apparent transaction cost is low, but the total transaction cost (roundtripping) can be prohibitive. Hence, the control premium still exists and cannot be easily traded away. Identifying tradable market inefficiencies is a topic that is beyond the scope of this book, and in all likelihood cannot be taught except through examples, which if they are widespread, are likely exploited and no longer exist. However, those would be a good starting point for you to develop your own ideas about market inefficiencies. Books by Jack D. Schwager, who has interviewed many successful investors, could be a good starting point if you want to understand how people viewed inefficiencies and what they did to exploit them. While identifying a tradable market inefficiency is more in the realm of art and serendipity, some aspects could be followed to avoid wasting time and effort. This would make the difference between climbing a hill or going down a rabbit hole! The following are some of the key questions to answer when validating an idea: Is the idea based upon a solid mathematical foundation or is it a spurious correlation limited in terms of time period and assets? If it is the latter, then it would work during certain time periods but with very little information about when it would stop working. An example is the correlation between the YenEuro exchange rate to European luxury goods stocks. This worked very well during the 90s but stopped working in 2000 when emerging markets consumers climbed up the consumption value chain and became the largest swing consumers. This type of analysis then leads to data crunchers using a lot of economic indicators to predict different economic sectors, stocks, currencies, and commodities, culminating in the development of leading indicators on leading indicators of economic data. What is the cost of implementing this strategy? Costs are a crucial element in an investment strategy. The difference between theoretical profit and costs is the money that will hit the bank. A great investment strategy built on costly implementation will result in a lot of resources being expended for no gain. While most people focus on transaction costs, the entire life cycle cost is crucial to consider.
[ 104 ]
Constructing, Testing, and Validating Models
Chapter 3
The costs of an investment strategy encompass the following: Personnel costs: How expensive the resources to develop and maintain the strategy. Infrastructure costs: What systems—development, production, trading, compliance, and accounting—are required and what each of them cost. Data costs: This cost is the most overlooked of the costs, partly due to subsidy by a parent organization or marketing that data is becoming free. Good quality timely data is expensive and required for a production environment. Transaction costs: How much it would cost to trade; considerations such as liquidity and depth of markets would play a major role. Costs of errors: This is generally not considered in detail, but a good investment model will have been stress tested for errors that are likely to creep in and looking at the cost of correcting those errors. The rest of this chapter will focus on the strategy of making a good idea a reality.
Selecting relevant financial models Model selection is both an art and a science. There is no science for identifying market inefficiencies. This is usually left to human intuition and creativity. The algorithms and analysis models on which they are based represent only an evolved tool through which to enhance human imagination in search of new investment and profit possibilities. A very basic model in signal processing is the Hidden Markov Model (HMM), wherein a time series is used to fit the parameters of a Markov process and hence understand the dynamics of the market. This class of models was developed for communications technology, in particular for encryptions and efficient transmission, but then used very widely by the scientific community in financial markets. D. E. Shaw and Co., Two Sigma, and Renaissance Technologies pushed the boundaries of this basic concept. A general rule of model development is to begin with the simplest model with a lot of assumptions about the underlying data set and market structure. Then, iteratively relax the assumptions one by one to keep on complicating the model until you get a model with as few assumptions as possible. It would be prudent at the end of this process to check that the remaining model assumptions are realistic and can be taken into account when trading the market.
[ 105 ]
Constructing, Testing, and Validating Models
Chapter 3
When none of the assumptions can be relaxed, given the construct of the model, move to a different type of model and repeat these steps. An example will help illustrate the preceding algorithm.
Example – factor models for return prediction It is wellknown that factor models (FM) are a manifestation of HMM. When constructing FM, a lot of assumptions are made about the data structure: Data is independent and identically distributed (i.i.d.) Data can be described by the normal distribution The error structure is Gaussian and random Preferences are meanreverting Here, the variable being forecast is the forward return for each of the factors that's relevant to the asset class, and the independent datasets are variables either based upon economic data or market data related to that asset class. We are assuming that the factors are measurable drivers, such as stock returns and bond returns. A typical process to construct such models would be to do the following: 1. Run individual regressions to test the explanatory power of individual factors. These are the partials of our model. 2. Sort the regressor variables by their explanatory power in descending order. 3. Keep on adding regressor variables one at a time, going down the list until the explanatory power does not show any improvement. 4. Consider the model obtained so far as final. We can use a validation dataset to note whether some efficiency deterioration has taken place. If the deterioration is large, then we can run individual regressions to understand which property of the test data did not translate to the validation data set. Once the test statistics are satisfactory, we can move on with the model development.
[ 106 ]
Constructing, Testing, and Validating Models
Chapter 3
During the model development phase, some a priori assumptions may be made about model structure, such as classifying factors into categories and limiting selection from a category to a maximum of n factors, to avoid overemphasizing one aspect of the model. For example, the model obtained in shown in the following screenshot combines value factors: momentum factors, and earnings quality factors using the preceding process:
An example of a factor model considering multiple variables
The factors used in the preceding model are as follows: adjbp_gw fcfap dpst4yr slspsgr1yr capexdepr globalbeta60 lnr121 lnr481 lnmv
Adjusted book to price growth Free cash flow to price Tstat of dividend per share over 4 years Sales per share growth over 1 year Capex to depreciation Beta of the stock relative to world index Log of 12 month return minus 1 month return Log of 48 month return minus 1 month return Log of market capitalization
This is the kind of linear, nopreference, nomemory model that has been used for over 30 years now. Deep learning allows you to develop a new class of models to overcome these limitations.
Adding nonlinearity to linear models The basic linear model can be improved by adding nonlinear relationships. Rather than use a maximum likelihood estimate with an assumption of a Gaussian distribution, we could model each of the factors using the Stable family of distributions (Gaussian is a special case of Stable), hence relaxing the second assumption of our linear model. Combined with this, we could now begin by understanding the correlation structure between each pair of variables using a method such as rank correlation and reducing an explanatory variable set.
[ 107 ]
Constructing, Testing, and Validating Models
Chapter 3
During validation, the new model's efficiency is computed with the validation data set and if found satisfactory, compared with the basic model. If there is no improvement in results, the basic model is used and our more complicated model is discarded.
Simple neural networks to capture nonlinearity and preference shifts The preceding models need constant estimation and judgment of modelers to determine when the factor selection needs to be altered. This can result in periods of deteriorated model performance, and returns would be dependent upon the manager of the model. Neural nets based on backpropagation algorithms are used to capture this sort of change in preferences. In this case, we begin with the structure of a linear or nonlinear model and combine that with the backpropagation of errors to tweak the weights of these factors. This makes the model reactive to changing market conditions.
DeepNets to incorporate memory in the modeling process All of the preceding models lack one thing—memory. It is well established that market regimes change over time and hence the parameters of the model need to react to take advantage of these changes. By developing a model using a test dataset with no learning, the regime changes are averaged and a model that would perform well on average is constructed. However, the ideal model would be one that has some memory about regime as an exogenous variable and can be comodeled with a factor model, hence creating a factor model for every regime. Deep learning models are good candidates to model this joint nonlinear structure because of the flexibility of using activation functions and connections in a deep network. The scientific aspect of model selection is based upon the type of problem being studied and the heuristics to simplify this process.
[ 108 ]
Constructing, Testing, and Validating Models
Chapter 3
Machine learning versus statistical models Statistical approaches and machine learning techniques both analyze observations to reveal some underlying process. However, they diverge in their assumptions, terminology, and techniques. Statistical approaches rely on foundational assumptions and explicit models of structure, such as observed samples that are assumed to be drawn from a specified underlying probability distribution. These prior restrictive assumptions can fail in reality. In contrast, machine learning seeks to extract knowledge from large amounts of data with no such restrictions. The goal of machine learning algorithms is to automate decisionmaking processes by generalizing (that is, learning) from known examples to determine an underlying structure in the data. The emphasis is on the ability of the algorithm to generate structure or predictions from data without any human help. An elementary way to think of ML algorithms is to find the pattern and apply the pattern. Machine learning techniques are better able than (parametric) statistical approaches to handle problems with many variables, that is, highdimensional data and/or with a high degree of nonlinearity. ML algorithms are particularly good at detecting change, even in highly nonlinear systems, because they can detect the preconditions of a model's break or anticipate the probability of a regime switch. While the field of statistical financial models is well researched, we can carry some of the learnings from statistical approaches to construct machine learningbased financial models.
Acquiring data from multiple sources Data is the lifeblood of any financial model. The edge that investment firms derive comes from their ability to acquire, organize, and analyze data. From Blackrock, the largest asset manager with its Alladin platform, to the reclusive Renaissance Technologies with its proprietary models for highfrequency trading and spectacular returns, all firms rely on data. There is a gamut of data providers who can provide a variety of data in a structured format for asset prices, valuation ratios, and analyst recommendations. Notable among those firms are Bloomberg, Factset, Worldscope, and IBES, to name a few. The adoption of social media by businesses has given rise to unstructured data being made available, which leads to structured data. This could be in the form of the following: News headlines on newswires Formal and informal chatroom text
[ 109 ]
Constructing, Testing, and Validating Models
Chapter 3
Video and audio feedback Tweets with the ability to move markets Social networks and media analytics The list is endless and here, the imagination of the modeler can take flight. However, a salient aspect of unstructured data is that it needs to be converted into some structured format before it can be used for predictive algorithms. Deep learning methods are very effective and naturally better suited to these applications than any statistical model. There is no alternative model in the statistical domain able to recognize a facial expression in response to the message being delivered. Even structured financial data has been found to be nonGaussian for most markets. Attempts have been made to fit other distributions to better explain the distribution capture the piecewise applicability of distributions, but not the correlation structure as it evolves over time. Some of the characteristics of financial data will be explained in the following sections.
Asynchronous Data is presented in a structured format and nicely aligned across time, but seldom appears in that order. When you download the quarterly earnings of companies as a time series, the assumption is that all of the data was available at the beginning or end of the quarter for all of the companies being analyzed. This is never the case. A few companies announce results every day. A key assumption when data is aligned in this fashion is that the results of one company have no impact on the prices of other companies. This is also never the case. If Intel announces great results, the prices of other companies in the sector are also likely to rise in anticipation, meaning they would announce good results as well. If any company does not live up to this expectation, then, even though the results are good, it would see an adverse impact on returns. So, when downloading data, you need to be careful to download both the data and the day it was announced to avoid the trap that you have asynchronous data for analysis.
Revised or backfilled Economic data is subject to revisions. Hence, when getting economic data, we need to be careful that the number being used was not visible at the time and was revised. A lot of statistical models that were based on economic data used to use lag relationships when constructing models, but a major flaw in this method is the use of incorrect data for analysis because the data used for prediction would, most of the time, not be revised data.
[ 110 ]
Constructing, Testing, and Validating Models
Chapter 3
Prone to manipulation Prices at open and close are subject to manipulation by highfrequency traders, depending on the imbalance in order books. Most exchanges report adjusted close prices, which take into account volumeweighted prices for the last half an hour of trade as an approximation to the close price. However, if you are training trading models, this might not be the data you require but the last few ticks of data as individual data points to train the model. While data can be averaged to result in an approximation, this might result in lost information.
Outliers Statistical models aim to unearth the structure of the underlying data by fitting some analytical distribution function to financial data. The maximum likelihood estimation method relies on fitting the distribution to a majority of the data points. This leaves out outliers. Hence, the process of Winsorization is widely adopted in statistical models when making predictions. Winsorization is a statistical procedure, due to Charles P. Winsor (18951951), which modifies the sample distribution of random variables by removing outliers, that is, those extreme, or "anomalous" values, that deviate significantly from the central values of the distribution. There are several implementations of this procedure. A very common one is scipy.stats.mstats.winsorize. However, in the GitHub repository of this book, we have made a more general version suitable for the purposes of this book available. The code winsorizes the data based on upper and lower cutoff values. You can define the upper and lower bound as asymmetric, as well as whether the data needs to be grouped or not. So, you can use that code to reject the top 5% and bottom 5% of the values as outliers for the entire dataset. Alternatively, you can also work on the top 5% and bottom 5% of the values of the outliers on a sector by sector basis. In doing the latter, if one of the sectors has values of the factors that are larger or smaller than the rest of the market, you avoid throwing away the majority of the observations of that sector. This has the effect of neglecting the outliers before making predictions. This is an approximation. Outlier structure does not remain constant during the estimation phase, hence using the approximation to discard the top and bottom x% of data points is heuristic rather than scientific. Moreover, this method results in incomplete datasets since all variables do not have the same distribution characteristic.
[ 111 ]
Constructing, Testing, and Validating Models
Chapter 3
Implementing the model At this stage, the model must be implemented using a particular software solution. The DL world, although recent, is already very rich in frameworks and tools. In this section, we will do a reconnaissance of the most popular solutions that are used to model and encode a deep neural network.
Keras Keras is a wrapper library that provides a common Python programming interface for popular DL backends, including TensorFlow, Theano, and CNTK. Developed and maintained by Francois Chollet since 2015, it is released under the MIT license. The main purpose of Keras is to facilitate experimentation, allowing models to run with different underlying computing engines on both GPUs and CPUs. The consistency and simplicity of the Keras API made it a very popular library among developers (in a 2018 KD Nuggets survey, it was found to be used by 22% of respondents) because of its ease of use and the ability to make models portable on different computing frameworks. As we will appreciate later in this book, a model in Keras is built by defining a sequence, or more generally, a graph, of independent building blocks that can be connected without particular restrictions. This allows us to quickly prototype complex models, including neural levels, activation functions, regularization schemes, loss functions, and solvers. From these basic building blocks, it is possible to build reusable modules that can be useful for the composition of complex models. Its strengths are as follows: Open source, with continuous updates Support for industrial backends such as Google TensorFlow and Microsoft CNTK, hiding implementation details and complexity of use Model decoupling and portability Wide adoption by developers and the scientific community Its weaknesses are as follows: Difficult to implement new architectures and custom modules that do not rely on basic building blocks Limited support for multiGPU
[ 112 ]
Constructing, Testing, and Validating Models
Chapter 3
TensorFlow TensorFlow is an open source framework created and managed by the Google Brain team. Its first release in the public domain dates back to 2015. It is currently released under the Apache 2.0 license. TensorFlow is weighted to realize vector numerical calculation by defining a computational graph through which the data, represented as tensors, that is, multidimensional arrays of data, goes through a series of transformations. Graph nodes that perform these transformations can be allocated to a cluster of heterogeneous nodes that include GPUs, CPUs, and TPUs. TensorFlow's architecture is similar to that of MapReduce, in that processing is performed by worker nodes that have a kernel implementation and are coordinated by master nodes. The kernel, written in C++, includes over 200 standard operations that implement operators and mathematical functions, tensor manipulation, control flow, and status management. This enables TensorFlow to perform largescale training and inference sessions with massive data. Its kernel can run on CPU or GPUequipped workstations, distributed systems with hundreds of nodes, as well as mobile and embedded devices. TensorFlow can be programmed using Python and C++ interfaces. It is also planned to have APIs for Java, Go, R, and Haskell. Its strengths are as follows: By far the most popular open source and rapidly evolving DL framework. Widely spread both for scientific experimentation and industrial applications. Developed and supported by Google, it is widely available in commercial cloud solutions such as AWS and Microsoft Azure. High scalability on distributed and heterogeneous computing infrastructures, regardless of CPU, GPU, and TPU. Works efficiently with mathematical expressions involving multidimensional arrays. Its weaknesses are as follows: Lowerlevel API and programming model not very easy to use Each calculation flow must be described through a static computation graph
[ 113 ]
Constructing, Testing, and Validating Models
Chapter 3
Theano Before the advent of TensorFlow, Theano has long represented the reference framework for DL. Its development started back in 2007 by the LISA group (now MILA), directed by Yoshua Bengio, and ended in 2017. It is an open source project with a BSD license. Theano is focused on a mathematical expression compiler, through which vector processing is expressed in code using NumPy, BLAS, and other native libraries with support for both CPU and GPU. Theano has extensions that allow multiGPU processing and distributed training. Its strengths are as follows: Open source, multiplatform project Widely used and appreciated by the scientific community Symbolic computing Its weaknesses are as follows: Without the availability of wrappers such as Keras, lowlevel APIs are not easy to use for direct template creation. No support for mobile devices and other programming environments except Python. No longer active.
Microsoft CNTK Microsoft Cognitive Toolkit (CNTK) is a commercialgrade framework developed and distributed under the MIT license by Microsoft Research, starting in 2016. It is designed for largescale DL modeling for speech, image, text, and handwritten data. Similar to TensorFlow processing vector data is expressed symbolically through a graph of tensor operations, such as the sum/multiplication of matrices and convolution. CNTK allows building models based on feedforward, convolutional, and recurrent neural networks. Training the networks is done through Stochastic Gradient Descent (SGD), with automatic differentiation. It natively supports multiGPU parallelization and is highly scalable on distributed computing infrastructures. CNTK is supported on both Linux and Windows 64bit environments and can be programmed through Python, C#, C++, and BrainScript interfaces.
[ 114 ]
Constructing, Testing, and Validating Models
Chapter 3
Its strengths are as follows: It is a rapidly evolving open source system, developed and supported by Microsoft. It has proven to offer better performance than Theano and TensorFlow when used as a backend in Keras in the implementation of RNN models, and LSTM in particular. It supports the Open Neural Network Exchange (ONNX) format. Its weaknesses are as follows: Limited support for mobile and embedded devices Open Neural Network Exchange (ONNX) is an open source project developed by Microsoft, Amazon, and Facebook to share a model representation format that enables an open ecosystem of interoperable frameworks and tools and interchangeable models. This format was initially supported by PyTorch, Caffe2 , CNTK, and MXNet.
PyTorch PyTorch is the DL framework developed and distributed under the BSD license by Facebook AI Research (FAIR), led by Yann LeCun, starting in 2016. PyTorch is written completely in Python and is based, for tensor computation, on the same C libraries (TH, THC, THNN, and THCUNN) previously used by another very popular framework, Torch. Also, PyTorch integrates acceleration libraries such as cuDNN and NCCL from Nvidia and MKL from Intel. This is a solution that is gaining wide acceptance and interest from both the scientific and industrial world, as it allows Keras to quickly build complex models. PyTorch, similarly to other modern DL frameworks, supports selfdifferentiation reverse mode. The solution that's implemented by PyTorch is inspired by what was done in Chainer, HIPS autograd and twitterautograd. The type of framework to which PyTorch belongs is characterized by dynamic, definebyrun, and immediate, eager execution. In practice, each tensor that needs to be calculated is immediately queued for CPU/GPU calculation. Also, crossing the block is recorded to keep track of it when backpropagation is applied. This approach is different from the defineandrun approach used by other frameworks, such as TensorFlow, that fully build the graph at the beginning.
[ 115 ]
Constructing, Testing, and Validating Models
Chapter 3
However, compared to other solutions, PyTorch has some optimizations that make it highly scalable. Unlike tapebased meters (also called Wengert lists), PyTorch uses a structure called Dynamic Computation Graph (DCG). DCG is acyclic and is generated at every iteration on the data: the roots represent the input tensors, while the leaves are the output tensors. Backpropagation is applied by running the DCG backward. Moreover, its basic logic is implemented directly in C++, with a significant performance improvement compared to other frameworks that use intermediate languages. Its strengths are as follows: An open source project widely supported industrywide by organizations such as Facebook, Twitter, Nvidia, and others. Automatic differentiation based on reverse mode, with support for NumPy and SciPy. Simple and flexible programming model. Supports the ONNX format. Its weaknesses are as follows: Mobile device support is still missing
Caffe2 Caffe2 is an open source framework developed by the Facebook Research team, led by Yangqing Jia starting from 2017, and distributed under BSD license. Caffe2 is a lightweight library designed to offer a simple and straightforward way to experiment with new algorithms and models. However, its qualities have made it a productiongrade solution on Facebook, while PyTorch is used for development. It is a crossplatform framework available for Mac OS X, Ubuntu, CentOS, Windows, iOS, Android, Raspbian, and Tegra. Caffe2 takes its name from its predecessor, Caffe, developed by Yangqing Jia himself while he was at the University of Berkeley. Compared to Caffe, Caffe2 differs in several optimizations. First of all, its programming model focuses on the operator concept, which offers more flexibility than the layer concept used in Caffe. Currently, Caffe2 has more than 400 operators, which makes it particularly functionally rich. Also, Caffe2 is designed to support new hardware in addition to traditional CPUs and GPUs. This makes it interesting for industrial applications that address the mobile and embedded world.
[ 116 ]
Constructing, Testing, and Validating Models
Chapter 3
Caffe2 provides a series of tools for porting models that were made in Caffe and Torch (the latter passing through a first translation in Caffe). Since 2018, Caffe 2 has become part of PyTorch to combine the user experience of the PyTorch frontend with the scalability, distribution, and embedding capabilities of Caffe2. Its strengths are as follows: Crossplatform, strongly mobileoriented and embedded, which makes it interesting for edge devices. Widely supported industrywide by Amazon, Intel, Qualcomm, and Nvidia Supports the ONNX format Its weaknesses are as follows: More difficult than PyTorch for beginners to use.
MXNet Apache MXNet has been incubated by Apache Foundation as of 2016. It is distributed under the Apache 2.0 license. MXNet is portable and lightweight, and can be scaled effectively across multiple GPUs and multiple machines. It supports programming interfaces for C++, Python, R, Scala, Matlab, Javascript, Go, and Julia. It is designed to be used in multiGPU and distributed environments. It can be used for inference on edge devices with low processing capacities such as mobile devices, IoT devices, and serverless applications. Launched by Pedro Domingos, it has seen the contribution of participants from many institutions and therefore has been immediately enriched with a variety of backgrounds and use cases that make it one of the most versatile frameworks on the scene. One of its main features is the possibility to combine symbolic and imperative programming. Inside, MXNet has a scheduler that determines functional dependencies dynamically and therefore automatically parallels symbolic and imperative operations on the fly. Also, it also has a graph optimizer, which makes symbolic execution faster and reduces memory occupation. Its strengths are as follows: Fully open source project supported by the Apache Software Foundation. High versatility and computational scalability for multiple GPUs and CPUs, making it very useful in business environments.
[ 117 ]
Constructing, Testing, and Validating Models
Chapter 3
Dynamic dependency planner that allows you to automatically determine the parallelization of the calculation. Flexible programming model and support for a wide variety of languages, such as C++, Python, Julia, Matlab, JavaScript, Go, R, Scala, Perl, and Wolfram. Supports the ONNX format Its weaknesses are as follows: APIs are not always intuitive and easy to use.
Chainer Chainer is an open source framework based on Python, developed since 2015 by the development team of Preferred Networks, Inc., and consisting mainly of engineers from the University of Tokyo. It is released under an MIT license. Like other frameworks, Chainer provides support for a wide range of DL models, including CNN, RNN, Reinforcement Learning (RL), and Variational Autoencoders (VAE). Chainer uses an automatic differentiation method based on the definebyrun approach, so a Dynamic Computation Graph (DCG) is created. The ability to achieve high performance is one of the features of Chainer. It uses CuPy to integrate with the CUDA/cuDNN libraries for Nvidia GPUbased solutions, while it uses MKL for Intelbased solutions. It also makes available libraries specifically designed for vertical industrial applications such as ChainerCV for computer vision, ChainerRL for reinforcement learning, and ChainerMN for multinode distributed environments. The latter enabled Chainer to outperform MXnet, CNTK, and TensorFlow in a ResNet50 training benchmark in a cluster of 32 nodes consisting of 4 GPUs each, for a total of 128 GPUs. Its strengths are as follows: A project with a strong industrial vocation with the support of investors such as Toyota, FANUC, and NTT Flexibility given by the definebyrun approach Provides highperformance libraries for vertical industrial applications Its weaknesses are as follows: The DCG is regenerated each time. Higherorder gradients are not supported.
[ 118 ]
Constructing, Testing, and Validating Models
Chapter 3
Torch Torch was one of the first scientific computing frameworks to be extended to support the DL. The project, which has been developed in C++ since 2002, has seen integration with CUDA support and the introduction of the Lua language as a wrapper for C/C++ code over the years. It is distributed under a BSD license, although its development finished in 2017. This project has been widely supported and used by Google, DeepMind, Facebook, Twitter, and many other organizations (and is still in production today) to process massive images, videos, sounds, and text. Torch uses the Tensor library for both CPU and GPU computing. This library uses optimized implementations of many tensor manipulation and processing operations, using BLAS/Lapack supported by Intel MKL for CPU or OpenMP and CUDA for GPU. Its strengths are as follows: Easy code reuse Good readability of the code and flexibility related to the use of the Lua language Fast and modular Much appreciated for research activities Its weaknesses are as follows: Less use than in the past LuaJITrelated integration issues Low diffusion of the Lua language within the development community No longer developed
Caffe Caffe is a framework that Yangqing Jia developed during her PhD at Berkeley University and then later until 2017. Caffe is the predecessor of Caffe2, so it anticipates some of its features. It is made available under a BSD license. The models are described by composing layers, which represent the main abstraction in Caffe's programming model. The input is provided through a data layer that can draw from multiple sources, including databases (LevelDB or LMDB), distributed filesystems (HDFS), or specific formats such as GIF, JPEG, PNG, TIFF, and PDF. Processing is done through common layers. It is possible to extend the framework by implementing new layers in C++ CUDA.
[ 119 ]
Constructing, Testing, and Validating Models
Chapter 3
Its strengths are as follows: Several pretrained networks are available through the Caffe Model Zoo. It's easy to program thanks to the Python and MatLab interface. Its weaknesses are as follows: The project is no longer active. Custom layers must be implemented in C++. The prototxt format that's used to save and read models is poorly suited to complex models such as GoogLeNet and ResNet. The computation model does not support many RNNcentric applications. The definition of the static model graph is not suitable for many RNN applications that require variable input sizes. Model definition in Caffe prototxt files is too cumbersome for very deep and modular DNN models such as GoogleLeNet or ResNet compared to other frameworks.
Wrappers The APIs offered by DL frameworks are usually oriented to offer a highperformance computing environment that often does not facilitate their adoption, especially in the testing and prototyping phases. Also, the APIs of each framework depend on different design choices, so they are usually different from library to library. Also, a wrapper can be designed for specific domains, hence simplifying common tasks such as data management, preprocessing, inference, and the use of specific metrics to measure and compare model performance. Finally, a wrapper can simplify the use of a framework in a specific environment, such as cloud and edge computing. To overcome these problems, some wrappers make the models independent of the particular backend framework being used and hide lowlevel implementation details. As we mentioned previously, Keras is the most popular solution, but there are other options. For example, for TensorFlow, there are several. Besides Keras, other solutions include TensorLayer, TFLearn, and TFSlim. Sonnet and PrettyTensor are two TensorFlow wrappers developed by Google DeepMind. For Theano, there is Lasagne, a lightweight library that simplifies the definition of models by facilitating their modularity and composition. Blocks and Pylearn2 are two other wrappers made available for Theano.
[ 120 ]
Constructing, Testing, and Validating Models
Chapter 3
A wrapper for MXNet users is Gluon. It was born from the collaboration of AWS and Microsoft to define a standard interface specification able to make the development of models independent from the particular backend being used. Nvidia developed Digits, a web application capable of defining datasets and models, then performing training and inference for image processing applications such as classification, object recognition, and segmentation. Digits can use Caffe, Torch, and TensorFlow as backends. It can read a variety of image formats and manage multiGPU learning, offering realtime performance monitoring. Also, pretrained models such as AlexNet, GoogLeNet, and LeNet are rendered. These are just a few examples of the various possibilities offered and it is difficult to predict future developments in a rapidly changing industry. However, two main trends are emerging as things stand. The first trend, promoted by Google, sees Keras as a prototyping environment and TensorFlow as a backend and for production. The second is promoted by Facebook and promotes the adoption of PyTorch for prototyping and as a backend, supported by Caffe2 for production. When choosing the software that you implement in a prototyping and production environment requires you to pay attention to performance both in terms of numerical accuracy and, more generally, training capacity and inference accuracy, but also looking at the use of computing resources in terms of time and memory. In other words, assuming we use the same data, the same training method, and the same hardware, how do frameworks and libraries compare? This aspect is often forgotten and all attention is paid to the model. In the last few years, several benchmarks and studies have been developed that allow us to compare, even if partially, various software solutions. The datasets that are normally used for this comparison are MNIST, CIFAR10, ImageNet, Microsoft COCO, Cityscape (computer vision), and IMDB review (for NLP). The models that are used typically MLP; CNNs such as ResNet, AlexNet, and GoogLeNet; RNNs such as LSTM and GRU; and stacked autoencoders. Finally, on the hardware side, the most common configurations use Nvidia GPUs, particularly the Tesla K80, NVIDIA GTX 1080, NVIDIA Tesla P100, and NVIDIA Titan X, which are typically available onpremise or through cloud services. The experimental results show that there is basically no significant difference in the accuracy achieved by using different combinations of wrappers and backends, partly because most use cuDNN as a lowlevel library.
[ 121 ]
Constructing, Testing, and Validating Models
Chapter 3
Evaluating investment strategy "It's a commonly accepted rule that a good financial model is the one which has a high Rsquare, significant tstats for individual factors but most importantly it should make money!! " Most financial modelers will compute a host of statistics to determine the efficiency of their models. We can evaluate the models for their statistical efficacy as the first step and financial characteristics to determine whether they are usable or not. Even some wellconstructed models might not be useful in generating returns; however, they could be useful in understanding the market structure and hence good for risk modeling.
Commonly used statistics The following are some of the commonly used statistics to evaluate the model and its significance: Statistic tstats Rsquared ANOVA
Significance How good are the explanatory variables individually. What is the explanatory power of the model. Testing the null hypothesis.
While the preceding statistics answer how good the data fits the model, it sheds no light on how the model would perform in reallife conditions. We'll explain some of the commonly used financial metrics to test the model in the following sections.
Commonly used financial metrics When financial metrics are computed for an investment strategy based on the tested financial model, the following are taken into consideration: Rebalance frequency: This cannot be lower than the estimation frequency. Trading costs: This depends on markets and asset classes. Other constraints, such as maximum and minimum holding sizes. Once these are incorporated, the model turns into an investment strategy and is evaluated using multiple criteria.
[ 122 ]
Constructing, Testing, and Validating Models
Chapter 3
Cumulative and monthly returns This is a basic evaluation. If the cumulative return of a strategy is negative, we can discard it right away. Even when the cumulative return is positive, monthly returns would give us an idea about the lumpiness of returns. Return profile preference depends on the investor's risk tolerance and overall investment strategy.
Information coefficient The traditional definition of the information coefficient that was used to assess the skill of an analyst or a portfolio manager was the proportion of correct predictions. The range varies from 1 to 1. An IC score of 1 indicates perfect prediction, while an IC of 1 indicates perfect inverse predictions. The formula for the information coefficient is as follows:
For quantitative models, rank correlation between predicted returns and realized returns are used to compute IC. Here, magnitude does not matter but the order is important. The investment logic of choosing a model with a higher rank correlation is that point forecasting has a large error. Hence, instead of estimating what the percentage return would be of each asset, if we know that one asset is likely to do better than the other, then allocating to this asset over the other would result in a better return profile. The following image illustrates the effectiveness of the raw signal and how that would translate into the effectiveness of the portfolio once constraints have been applied for portfolio construction. The general idea is to look for improvement in portfolio construction by reducing the volatility of the portfolio due to diversification. In the below image you will see IC for raw signal and IC for optimal portfolio:
[ 123 ]
Constructing, Testing, and Validating Models
Chapter 3
Information coeﬃcients of the model and its translation to the portfolio
The information ratio and Sharpe ratio The information ratio and Sharpe ratio capture the unit of return per unit of risk: IR=(Portfolio Return−Benchmark Return)/Tracking Error
[ 124 ]
Constructing, Testing, and Validating Models
Chapter 3
Here, we have the following: IR: Information ratio Portfolio Return: Portfolio return for period Benchmark Return: Return on fund used as a benchmark Tracking Error: Standard deviation of the difference between portfolio and benchmark returns The following is the formula and calculation for the Sharpe ratio:
Here, we have the following: Rp: Return of portfolio Rf: Riskfree rate σp: Standard deviation of the portfolio’s excess return The former works on returns and risk relative to a benchmark while the latter works on absolute returns and absolute risk. Risk is the probability of incurring a loss, hence if we are compensated better for the risk we take, on average, we would not incur a loss. This is a very important measure reported by most portfolios. The following chart in the following image illustrates the information ratio for each of the industry sectors and the corresponding alphas (excess return over the benchmark). The alpha is divided by 10 to keep the scale uniform. Typically, we are interested in sectors where the IR is greater than 0.4 and would like to constrain the turnover to reflect that metric. Here, the IR for one of the sectors is negative. This would call for model reestimation while using this sector for obtaining better results:
[ 125 ]
Constructing, Testing, and Validating Models
Chapter 3
Information ratio and excess return by the industry sector
Maximum drawdown Everyone loves a positive return. But occasional drawdowns are a reality of any money management strategy. If the return is 50%, then the asset needs to double just to break even. Hence, investors prefer strategies that have lower drawdowns. Institutions work on maximum drawdown rather than absolute drawdown, that is, drawdown from the peak value to the bottom value of the strategy's cumulative returns. While this might not discourage investing in a strategy, it gives us an insight into when money should be withdrawn from a strategy or when money should be added to a strategy. It is a crude way of identifying turning points but prevalent.
[ 126 ]
Constructing, Testing, and Validating Models
Chapter 3
Sortino ratio This ratio is very fashionable right after a financial crash. The basic premise for the ratio is that people like positive returns and dislike negative returns. Hence, from the monthly returns, if we exclude the positive returns and compute the downside return divided by its variance, we would get an understanding of the downside measure of the portfolio. The preceding measures are also computed for subsections of the investment strategy to determine the effectiveness based on the section. For example, the model performance might come from two or three subsectors of an index. Hence, the strategy can be tweaked to take risks only on assets belonging to those sectors while keeping the other assets neutral and hence conserving turnover and saving trading costs. Testing diagnostics can give insights into an investment strategy that can be used to tweak the investment strategy for optimal returns. An example of summary statistics is given in the following screenshot:
Figure 5  An example of summary statistics
In the following chapters, some basic testing methods will be employed but we must keep the preceding in mind when building an investment strategy. Diagnostics can be another book in its own right.
[ 127 ]
Constructing, Testing, and Validating Models
Chapter 3
Tuning the model The performance of a model depends on the structural and optimizer characteristics that are determined based on values that we have learned to call hyperparameters. These include the learning rate, the number of layers, the number of units (neurons), the size and squeaks of kernels, the number of futures planes, the activation functions, and so on. By modifying them, performance can improve or worsen, depending on the model's characteristic of capturing and generalizing the dynamics of the data it observes sufficiently. Tuning a model aims to determine an optimal mix of hyperparameters capable of getting the best performance from a model.
Grid search In grid search, a set of possible values is chosen for each hyperparameter. The model is tested for each possible configuration of the hyperparameters, that is, for each possible combination of the values they can assume. The combination that offers the best performance is used to instantiate the optimal model. For example, suppose you consider LearningRate, BatchSize, and HiddenUnits as hyperparameters of an MLP model with only one hidden layer. In particular, let's assume the following: LearningRate = [0.01, 0.02, 0.05, 0.1] BatchSize = [100,150,200] HiddenUnits = [50, 100, 150] They create a grid composed of 4 x 3 x 3 = 36 hyperparameter configurations: [0.01, 100, 50] [0.01, 100, 100] [0.01, 100, 150] [0.01, 200, 50] ... [0.02, 150, 50] .... [0.1, 200, 100] [0.1, 200,150]
[ 128 ]
Constructing, Testing, and Validating Models
Chapter 3
In Keras, the grid search can be implemented by using scikitlearn's GridSearchCV function. As an example, let's suppose we aim to optimize the hyperparameters of the MLP model we presented in Chapter 2, Designing Neural Network Architectures. The following code shows how to implement it: 1. As a first step, we import the required modules: from sklearn.model_selection import train_test_split, GridSearchCV
2. Then, we have to define a builder that, given the hyperparameters, provides an instance of the model: # create model def create_model(learning_rate=0.01, hidden_units=16): model = tf.keras.Sequential() model.add(tf.keras.layers.Dense(32, activation='relu', input_dim=11)) model.add(tf.keras.layers.Dense(hidden_units, activation='relu')) model.add(tf.keras.layers.Dense(1, activation='relu')) optimizer = tf.keras.optimizers.RMSprop(lr=learning_rate) model.compile( loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy']) return model
3. The builder is given as an argument to KerasClassifier, along with the number of epochs=10 and the verbose=0 level (silent mode): template = tf.keras.wrappers.scikit_learn.KerasClassifier( build_fn=create_model, epochs=10, verbose=0 )
The result is template, which we can use to instantiate models with different hyperparameters. 4. To create a grid, we first define the possible values that learning_rate, batch_size, and hidden_units can assume. Then, these dimensions are placed in dict: learning_rate = [0.01, 0.02, 0.05, 0.1] batch_size = [100, 150, 200] hidden_units = [8, 16, 32] param_grid = dict(
[ 129 ]
Constructing, Testing, and Validating Models
Chapter 3
learning_rate=learning_rate, batch_size=batch_size, hidden_units=hidden_units )
5. We are now ready to run the grid search: #load the dataset X, Y = load_dataset() #grid search gs = GridSearchCV(estimator=template, param_grid=param_grid, n_jobs=1, cv=3) gs_result = gs.fit(X, y)
It is worth noting the use of two parameters in GridSearchCV: n_jobs=1, for using all processors (cores) in parallel cv=3, for a threefold (stratified) crossvalidation
6. The optimal combination is given by looking at the result of gs.fit, as shown here: #summarize results means = gs_result.cv_results_['mean_test_score'] stds = gs_result.cv_results_['std_test_score'] params = gs_result.cv_results_['params'] for mean, stdev, param in zip(means, stds, params): print("mean_acc: %f std_dev: (%f) hyper_parameters: %r" % (mean, stdev, param)) print("Best: %f using %s" % (gs_result.best_score_, gs_result.best_params_))
This approach suffers from the combinatorial number of all possible configurations. This problem is exacerbated by the number of hyperparameters to be optimized. In fact, each configuration is required to instantiate the corresponding model in order to train and test it. Since a deep network requires time to be trained, it is easy to understand how quickly the overall time to optimize hyperparameters can grow. However, the grid search makes one thing clear: since each model is independent of the others, you can test each configuration in parallel with the others. Therefore, since you have a computing infrastructure with a sufficient number of nodes, the elapsed configuration optimization time is equal to the optimization time of a single model. In general, the more calculation resources are available, the more configurations you can test at the same time.
[ 130 ]
Constructing, Testing, and Validating Models
Chapter 3
Random search An alternative method is to randomly generate the configurations within the hyperparameter space and test the related models. This approach can be implemented by setting the number of configurations to be tested, but in practice, it is more useful to set a time limit for the optimization activity. Once the limit has been reached, the configuration that gives life to the best model will be proposed. Obviously, this algorithm can be executed in parallel byexploiting the independence in model generation and evaluation. Therefore, also in this case, having a high computational capacity allows us to try a greater number of configuration and therefore allows us to select a better model. We can implement a random search by introducing a small change to the code used a grid search: # Define hyper_parameters values learn_rate = [0.01, 0.02, 0.05, 0.1] batch_size = [100, 150, 200] hidden_units = [8, 16, 32] param_grid = dict( learning_rate=learning_rate, batch_size=batch_size, hidden_units=hidden_units ) rs = RandomizedSearchCV( estimator=template, param_distributions=param_grid, n_iter=10, n_jobs=1, cv=3 ) rs_result = rs.fit(X, y)
In this case, we specify n_iter=10 as the number of trials to performs, that is, the number of hyperparameter combinations to test. RandomizedSearchCV has plenty of other options that are worth looking at in the documentation.
[ 131 ]
Constructing, Testing, and Validating Models
Chapter 3
The interesting thing to observe is that a random search, with the same number of tested configurations, allows us to obtain better models than a grid search. Those familiar with sampling should not be surprised by this result. When optimizing the models, the interpretation given to this result is that it is easier to break the dependence between hyperparameters since each configuration will be composed of random values and is, therefore, more effective in space exploration than the hyperparameters. Otherwise, in the case of a grid search, the regularity of the grid will tend to replicate dependencies between parameters so that irrelevant factors will be unnecessarily reconsidered during the optimization process. As the number of hyperparameters increases, these dependencies become more likely, so if the number of four or five hyperparameters is exceeded, the adoption of random search should be considered.
Bayesian optimization As highlighted previously, the configurations considered by grid search and random search are all independent of each other. While this naturally allows the hyperparameter space to be searched for in parallel, it does not allow the search to be directed based on intermedial results. Each new attempt is independently developed as if the search was starting from scratch. Among the methods that have received the most attention recently is Bayesian optimization. This method, dating back to the 1970s, builds a probabilistic (surrogate) model of where the optimum is most likely to be found based on evidence. In our case, the evidence is provided by the tested configurations. With each iteration, the surrogate will improve the accuracy of the prediction. In the same way as grid search and random search, the search continues within a fixed number of iterations or when the time limit has been reached. Bayesian optimization consists of four elements: An objective function (what we want to optimize): The performance of the model, given a configuration of hyperparameters A search space (set of possible solutions): The space of the hyperparameters on which the model performance depends An optimization algorithm (how to build the surrogate): The method from which we get the next configuration to try out History of the results (the set of solutions): the set of configuration/performance pairs already determined
[ 132 ]
Constructing, Testing, and Validating Models
Chapter 3
The target function maps the hyperparameter configurations of the search space to the values used to measure the model performance. This link is neither known nor explicit. It can only be determined pointblank once a configuration is determined, the model is allocated, trained, and tested. Since the target function is unknown, the method assumes it as a random function, so each configuration is associated with a possible average and a possible standard deviation of the value of the target function. Each time a hyperparameter configuration is tested, it represents a point of evidence that makes up the history of the optimization process. Therefore, it represents a constraint point through which the average with zero variance passes. There are several methods to estimate the average and the standard deviation between known points. Among the most common is the Kringing or Gaussian Process (GP) regression method. In this method, mean and variance are estimated by interpolation of known points. Another more efficient method that is gaining interest is the Treestructured Parzen Estimator (TPE). Whichever method is chosen, the optimization strategy involves choosing a point that is the most promising to provide a very good one at each iteration. This is done through an acquisition function that will provide the configuration of hyperparameters to be tested at the next step. Among the various acquisition functions, the most common choice is Expected Improvement, which provides the point at which the maximum or minimum of the mean +/ STD value corresponds to the nature of the performance measurement (for example, accuracy or error). The new point, once evaluated, will represent a new constraint to be taken into account in the new estimate of the mean and variance at the next iteration. The process ends when the maximum number of iterations have been reached or the time budget has been exhausted. We can implement a hyperparameter Bayesian optimization using the Hyperopt package. First, we include the required components: from random import uniform from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
Then, we define the hyperparameter search space by attributing the hp.uniformint distribution to hidden_units and batch_size and the hp.uniform distribution to learning_rate: space = { 'hidden_units': hp.uniformint('hidden_units', 8,32), 'batch_size': hp.uniformint('batch_size', 100,200), 'learning_rate': hp.uniform('learning_rate', 0.01,0.1), }
[ 133 ]
Constructing, Testing, and Validating Models
Chapter 3
From the preceding space, Bayesian optimization will generate combinations of values for the hyperparameters of interest. These will be used by the following f_nn function to create, train, and test the model: #create model def f_nn(params): model = tf.keras.Sequential() model.add(tf.keras.layers.Dense(32, activation='relu', input_dim=11)) model.add(tf.keras.layers.Dense(params['hidden_units'], activation='relu')) model.add(tf.keras.layers.Dense(1, activation='relu')) optimizer = tf.keras.optimizers.RMSprop(lr=params['learning_rate']) model.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy']) model.fit( X_train, y_train, epochs=5, batch_size=params['batch_size'], verbose = 0) pred_auc = model.predict_proba( X_test, batch_size = params['batch_size'], verbose = 0) acc = roc_auc_score(y_test, pred_auc) print('Accuracy:', acc) sys.stdout.flush() return {'loss': acc, 'status': STATUS_OK}
A search is performed using the fmin optimizer provided by Hyperopt, using the following parameters: f_nn, the model generator and tester space, the hyperparameter search space algo=tpe.suggest, the Treestructured Parzen Estimator (TPE) max_evals=50, the limit of evalutation
[ 134 ]
Constructing, Testing, and Validating Models
Chapter 3
Let's take a look at the following code: #search and output results best = fmin(f_nn, space, algo=tpe.suggest, max_evals=50) print('best: ') print(best)
Unlike grid search and random search, this approach is essentially sequential. For this reason, it is also called Sequential Model Bayesian Optimization (SMBO). However, it is possible to speed up the acquisition of points by evaluating multiple configurations at each iteration, for example, by choosing multiple configurations, appropriately distributed in the search space and sorted by decreasing values of expected improvement.
Going live Once an investment strategy has been tested, as mentioned in the aforementioned steps, it is time to take it live. Going live means committing capital to an investment strategy and making sure there's business continuity for trading activities. So, there is a need to move from the prototyping environment to the production environment. The two are different, as outlined in the following table: Prototyping Objective
To explore and test candidate models.
Nature
Experimental. Poorly structured and unorganized environment. Historical data provided as datasets on files or thirdparty services.
Maturity Data Links
Offline or with limited outbound connection.
Computing Intensive with no realtime constraints. Low/medium, because of costs with no return in R&D activity due to highly skilled Risks personnel, data acquisition, and computing infrastructure. Team Quants.
[ 135 ]
Production To ensure business continuity to trading activity. Operational. Stable and fully documented environment. Live market and companion data feeds. Integrated with the banking system and market operators. Moderate with realtime constraints. High, because of potential losses due to not envisaged adverse market conditions, hacking activity, and business discontinuity. Trading desk, portfolio managers.
Constructing, Testing, and Validating Models
Chapter 3
The two environments have different purposes. The prototyping environment serves the purpose of developing an environment through a substantially trialanderror approach. This means that its advanced laboratory experimental nature translates into a poorly structured and organized environment. On the other hand, the production environment must ensure full continuity of trading activities. This means that it is designed at an operational level so that the solutions (models and code) hosted within a production environment must be stable and fully documented. The prototyping environment is generally the realm of quants, while the production environment is of the realms of the trading desk and portfolio managers. One aspect that is sometimes underestimated is the different nature of the data used. In the prototyping environment, the data is generally historical as it is necessary to develop training and simulation activities that work on a compressed time scale and with the ability to look forward. This is not compatible with online data provisioning. The data is made available as datasets on files or remotely accessible through providers specialized in this type of service. Bloomberg and Quandl are very popular choices. Instead, in the production environment, the data is acquired live from the market or other news and economic data sources. In the era of big data, social media and other unconventional sources are also gaining more and more interest from operators. Therefore, the prototyping environment typically presents a low connectivity to the outside world, typically limited to the acquisition of data that's useful for experimentation, while the production environment must be integrated with market operators, the banking system, and all counterparties involved in trading activities, while also taking into account the different rules and regulations of the markets at a national level. As far as the calculation infrastructure is concerned, it is not surprising that the prototyping phase is particularly demanding as it requires the development of largescale simulation and training activities. However, it does not usually take tight time constraints into account. On the contrary, since already trained models are hosted in the production environment, the computational intensity is reduced, even if the portion of data on which to apply the model could grow significant for the sample that's considered during the prototyping phase. During the full operation of the models, it is necessary to take the timing of transactions into considerations, which requires the respect of more or less stringent realtime constraints.
[ 136 ]
Constructing, Testing, and Validating Models
Chapter 3
Finally, the prototyping environment must take into account only the cost losses associated with the development of the models without a certain return from that activity. These costs, in the field of deep learning, are substantially linked to the need to coalesce highly qualified skills that are not easy to find on the market, given the competition with industrial sectors that were once distant but now need to use advanced AI techniques in their solutions, highquality data and annotated in large quantities, which often require significant human resources to be produced, and onpremise or cloud computing resources. Conversely, a production environment has to deal with the potential losses related to the reality of laboratory designed models that could face adverse and unforeseen market conditions. Also, a new challenge to algorithms is coming from AI being used to implement hacking activities to induce a specific behavior in target models. Business discontinuity can be an additional source of potential losses. Generally, the tools that are used in the test environment—hardware and software—comply with a firm's security policy. However, when moving to the production environment, a typical investment firm will follow the following steps as a part of the going live process (not in a strict order): 1. 2. 3. 4. 5.
Documenting investment strategy and code Transitioning toward the production environment Testing with paper portfolios Soft launch Go live
Obviously, in reality, things are more complex and the go live process is more articulated than what's presented here. However, for this book, we believe that the following description is sufficient to give a broad idea of what it means to make an operating model in a financial environment. This also helps to project the reasoning that is done during the development of a model.
[ 137 ]
Constructing, Testing, and Validating Models
Chapter 3
Documenting investment strategy and code As we've seen, working on models means experimenting with different working hypotheses, often going back on your own steps, using different data or preprocessing them in different ways, and changing libraries and frameworks. This does not help keep development documented, however disciplined it may be. It is also common that the code has inactive portions, such as unused functions and variables or commented lines of code, not removed and left inside for potential future use. This behavior is very common when writing prototype code. Finally, the code used for experimentation is usually structurally weak, precisely because of the constant changes and the attention paid more to results than to the way they are obtained. Unfortunately, due to its idiomatic possibilities, scripting languages such as Python do not help to maintain a solid structure of code. This requires regulating code writing as much as possible and considering careful refactoring at the end of development. Therefore, during the transition to a production environment, you need to carefully document the strategy and the code that implements it, after cleaning up and refactoring it. Specifically, it is necessary to document the following: Hardware and software requirements need to be documented, including the list of software libraries from third parties, such as Python packages, frameworks, native libraries, and other extensions. It is also necessary to document clean code files—commented and following guidelines for software development. If code needs to be compiled, then use the document compiler options. Document data files that are used for model development and test plans based on the dataset. Document unit and system test plans and the results of testing.
Transitioning to a production environment Generally, this is a machine that has the same configuration as the production environment but not linked to the banking systems or external counterparties. This machine is monitored for all communication that is logged. This is done to prevent any Trojan or malware that might be residing in the code or dependent libraries. Code—compiled or interpreted—is run in this environment at a higher frequency to simulate the test conditions, and the results are matched for precision with those of the test environment. Typically, but not always, the team that conducts tests in the pseudoproduction environment is independent of the team that has developed the strategy. This prevents any lastminute workarounds that have not been documented to slip into the production system. It serves as a quality check.
[ 138 ]
Constructing, Testing, and Validating Models
Chapter 3
Once the tests have been conducted to satisfaction, migrating to the production environment is as easy as copying the image from the transition environment to the production system.
Paper portfolios Once migrated to the production environment, an investment strategy is run using fictitious capital. This aspect is known as a paper portfolio (a portfolio that is on paper only and no capital has been committed yet). This is an important step to determine whether there is any mismatch between the assumptions about trading conditions during the test phase and the current market scenario. Paper portfolios work like a live investment strategy, with the investment team making investment decisions, and those being passed on to the execution desk for live pricing. These trades are not executed; however, the traders or market makers would enter fills based on the live conditions. Portfolio performance is subjected to the same rigor as that of a portfolio with real capital—risk and compliance controls, performance reporting and attribution, performance reviews and explanations, and so on—to provide the team with experience of realworld conditions. If the paper portfolio results are within the tolerance limits of test results, it is time to commit real capital. This is done in two stages.
Soft launch The soft launch process involves the marketing team of the firm, to explain the strategy and prepare an investment prospectus for potential clients. This needs to be vetted by the legal and compliance teams to conform with regulations of jurisdictions where the fund is likely to be marketed when seeking capital. During the soft launch process, either the investment firm commits a limited amount of capital or finds a seed investor to commit initial capital with a lockin period at very attractive terms. The strategy is then run using this capital and follows all of the investment processes and reporting requirements. The strategy is now generating a live track record. This can be published in several databases that track manager performance.
[ 139 ]
Constructing, Testing, and Validating Models
Chapter 3
Go live! After a few months of the strong performance of the soft launch portfolio, it is time to market the strategy to a larger audience and increase the capital that's been allocated to the strategy. This presents challenges of scaling up the portfolio without sacrificing performance. Paper portfolios and soft launch portfolios have a key advantage visavis a live strategy, with the most significant being a timecontrolled allocation and redemption of funds. Live strategies do not have this luxury. Inflows of new money to an investment strategy does not follow a strict calendar routine. Money can be allocated to a fund any time during the month without regard to volatility. This creates deployment pressures. The entry load amount may not be enough to cover the transaction costs for the deployment of funds. Implementation generally follows the process wherein cash is first allocated to liquid assets most correlated with the fund strategy. For example, if the fund is investing in stocks, then the inflows will be allocated to indices that track the strategy universe as a costefficient way of gaining market exposure and then liquidating those index positions when the underlying stocks are being purchased. This reduces the drag on the portfolio that occurs when cash is held but does not eliminate the drag. It adds to the complexity of running a live portfolio, which is not present when paper portfolios are traded. The simultaneous management of currency, futures, and underlying exposures while not being in breach of any investment guidelines is a challenge faced by fund managers daily. Live strategies also suffer from an exit drag when the fund flows are imbalanced. Hence, strategies might be required to sell during periods of high volatility or adverse performance periods, resulting in having to sell underperforming positions, creating further pressure on fund performance and creating a downward spiral. Like entry drags, there is no mechanism to reduce the exit drag. Various strategies can be employed to predict the market conditions and benefit from exit drag, but they add to the risk of the mandate and hence not carried out in practice. Cash management during the paper portfolio phase prepares the implementation team or fund manager for these situations. All the experiments during the paper portfolio stage come to bearing during this phase. Typically, paper portfolio stages would have simulated these conditions in detail as a dry run for the implementation phase when external money is committed to these strategies.
[ 140 ]
Constructing, Testing, and Validating Models
Chapter 3
Benchmarking An investment strategy may deviate from model performance due to a variety of factors. It is very important to constantly benchmark live performance to backtest results as an early warning signal. Benchmarking needs to be carried out for the following: Data Model structure This is to capture the interaction effects between those.
Benchmarking live data A model's construct depends on the data that is used to estimate the parameters of the model. The confidence intervals around estimated parameters depend on the data that has been used to estimate the parameters. If the underlying data process changes, then it could result in one of the two alternative actions: Parameters of the data generating process have changed: Here, the distribution structure of the data remains the same but the parameters of the distribution change. This would mean not changing the structure of the model, but reestimating the parameters and the confidence interval around those parameters. The model structure is robust to such changes since the error structure has not been altered. When stocks transition from a low growth phase to a high growth phase, assets that had low investor interest move to higher investor interest by events such as index inclusion and so on; the underlying process has not changed but the parameters of the underlying process have changed.
[ 141 ]
Constructing, Testing, and Validating Models
Chapter 3
Data generating process changes: When the underlying process generating the data changes, this results in a change in the model structure. So, when the stochastic process that is generating the data has altered due to a structural shift in realworld dynamics, the model parameters are no longer reliable and is the model's structure isn't robust. This would mean going back to the drawing board to estimate the model and its parameters and conducting tests all over again. Quantitative easing has been a structural change that has impacted assets by changing the dynamics of the process altogether. Hence, most of the models that were calibrated on data without the prevalence of QE needed to be reestimated for both structure and parameters. It is important to check for the difference in data processes regularly to catch errors early. We need to do this since portfolio drifts generally delay the effects of a change in a data process that are reflected in returns for a long time. When such changes are detected, the damage would be generally large and outside the range of performance statistics.
Benchmarking to model diagnostics This exercise is carried out by using the model diagnostics as a benchmark. The basic concept is to use the model diagnostics over time to create a distribution of all the parameters of the diagnostic and then map the live portfolio diagnostics to this distribution. The Z score of the live performance will reflect the efficacy of the model in the current situation. Model deterioration can occur due to the following: Crowded trades: If many strategies pursue the same assets, then there is a crowding effect in those assets resulting in higher impact costs and hence reduced returns. Changes in risk tolerances resulting in changed position sizing: Classification of an investment strategy may alter, depending on the dominant investor base, resulting in changes required to position sizes, thus altering the return profile. Parts of the model universe being rendered uninvestable: Changes to index compositions such as inclusion or exclusion of countries or sectors can result in previously included assets being removed from the investable universe. We need to be vigilant about these alterations. If the live performance diagnostics are at extreme ends of the distribution of model diagnostics, it will definitely require a reestimation of the model and an inspection to understand why the performance parameters vary so much. The following chapters will be exploring certain sections we presented earlier in more detail.
[ 142 ]
Constructing, Testing, and Validating Models
Chapter 3
Summary In this chapter, we have taken a look at the development cycle that leads on from the identification of a trading opportunity to the definition of a model, its implementation, validation, and optimization. The model that's obtained this way can then be deployed and used. We also observed that because market conditions tend to change over time, a model may lose its effectiveness. This requires it to undergo periodic reviews, which may also require some of its implementation steps to be revised until it is decommissioned. Starting from the next chapter, we will go into detail about some models that, based on deep neural network architectures, can be applied to different problems.
[ 143 ]
2 Section 2: Foundational Architectures This section is devoted to building simple models based on primary architectures. We will start by replicating an index by means of autoencoders. Then, we will move on to implementing a model for daily volatility prediction based on long shortterm memory (LSTM). Finally, we will approach convolutional neural networks (CNNs) to identify trading rules. This section comprises the following chapters: Chapter 4, Index Replication by Autoencoders Chapter 5, Volatility Forecasting by Long ShortTerm Memory Chapter 6, Trading Rule Identification by CNN
4 Index Replication by Autoencoders Fund managers and investors are increasingly adopting passive investing strategies. In fact, according to Moody's Investors Service, passive investments in the U.S. will surpass active investments by 2021. Passive investing is a buyandhold strategy that involves buying an asset (or set of assets) to hold it for a longterm horizon. Index replication or index tracking is one of the most popular methods belonging to this group. It is aimed at matching the performance of the benchmark index while minimizing the error. The problem faced in this chapter is how to replicate an index with a small subset of stocks belonging to such an index with minimal tracking error. The subset of stocks is identified by using an autoencoder (AE). We will focus on creating and training a vanilla AE to minimize the reconstruction error and how it can be used for tracking a market index. This chapter will provide some considerations about the internal architecture of the AE and its hyperparameters, and it will also illustrate the implementation aspects regarding some AE variants. Special attention will be paid to deep AEs. This chapter will include the following topics: Replicating an index Data gathering Implementing a vanilla AE Replicating an index by using an AE Exploring some AE variants Understanding deep AE
Index Replication by Autoencoders
Chapter 4
Replicating an index Index investing is probably the most popular passive investment strategy. Since a market index cannot be directly traded, it is a common practice to create a welldiversified portfolio of securities that best replicate the index performance. This is known as index replication, indextracking, or indexing. Alternatives are to buy an index fund or to trade on an ExchangeTraded Fund (ETF) that tracks some broad index, such as SPY, which is intended to replicate the SP 500 or QQQ that tracks the NASDAQ100. The main advantages of these passive strategies are the low fees and simplicity, due to the lack of active management, but they usually entail small returns. A stock index is a measurement that represents the aggregated value of a collection of stocks that describes a particular market or a segment of it. The value of a stock market index is computed from the prices of stocks that compose it according to some weighting schema. The two main approaches are as follows: Priceweighting: It is computed as a weighted average of the stock prices, such as in the case of Dow Jones Industrial Average (DJIA). Capitalizationweighting: Here, stocks are weighted based on their total market capitalization, which refers to the total number of common shares outstanding multiplied by the price per share, such as SP500 and NASDAQ Composite. There are several other schemes, including the equalweighting of stocks, which assigns the same weight to all components in the portfolio. As mentioned earlier, it is not possible to directly invest in a market index. To gain access to the selected index, we may construct a diversified portfolio of stocks that tracks its performance by following the value of the index, called a tracking portfolio. This is, for instance, the first step to create an ETF. Index tracking is useful to invest in an index that has not associated any index fund or ETF. Also, a tracking portfolio is not as limited as the alternatives since we can adjust its performance. There are several methods to replicate an index. We can distinguish the following two groups: Physical replication: It consists of indirectly investing in the stocks. Synthetic index replication: It is based on using derivatives. A derivative is a contract between two or more parties whose price derives from fluctuations in the underlying financial asset (such as security) or group of assets (such as an index).
[ 146 ]
Index Replication by Autoencoders
Chapter 4
The first group, in turn, includes full replication and partial replication. The full replication technique is the simplest method since it requires us to purchase all of the stock belonging to the index. It can achieve an exact index replication, but it entails significant costs and may be difficult to manage. Also, it may include illiquid stocks, increasing the risk. Partial replication solves these issues by using a small number of stocks. In this case, the tracking error is arguably greater than under full replication, but partial replication reduces transaction costs and tends to avoid illiquid stocks. In the following section, we will illustrate how to gather historical financial data and how to store it to use it later in the experimentation of the model.
Data gathering To begin, let's download the historical data regarding the NASDAQ100 index and the stocks that compose it. As we have done in the example of Chapter 1, Deep Learning for Trading 101, we will use pandas_datareader to gather the price data from Yahoo! Finance and the pandas library to collect and save the time series. We will also need the NumPy library: import pandas as pd from pandas_datareader import data as pdr import numpy as np
We are interested in prices from January 1, 2013, to December 31, 2018, for a total of 6 years. Therefore, we consider only the stocks whose data is available for such a period of interest, apart from the index. You can find the list of corresponding symbols in this book's repository at the following link: https://github.com/PacktPublishing/HandsOnDeepLearningforFinance. The following code snippet reads the list and defines some variables of interest: # Stocks symbols df = pd.read_csv('data/nasdaq100_stocks.csv', header=None) stocks_symbols = np.squeeze(df, axis=1).tolist() # Index symbol index_symbol = '^NDX' # Dates start_date = '20130101' end_date = '20181231'
[ 147 ]
Index Replication by Autoencoders
Chapter 4
Note that this list of NASDAQ100 components may not be updated at the time of reading. You can find an uptodate version at the following link: https://www.nasdaq.com/marketactivity/quotes/nasdaqndxindex. Now, we are ready to download the price series and collect them in a pandas DataFrame. First, we download the index data, as follows: # Download index data data = pd.DataFrame() # Empty dataframe data[index_symbol] = pdr.DataReader(index_symbol, 'yahoo', \ start_date, end_date)['Adj Close']
Then, we download all of the stocks on our list. We include the data extraction inside a Python try and except clause to avoid a program crash if the internet connection fails. If there is some error with the connection, we add a delay in the execution of the program for 30 seconds by using the Python time.sleep function. Have a look at the following code: i = 0 while i < len(stocks_symbols): print('Downloading.... ', i, stocks_symbols[i]) try: # Extract the desired data from Yahoo! data[stocks_symbols[i]] = pdr.DataReader(stocks_symbols[i], 'yahoo', \start_date, end_date)['Adj Close'] i +=1 except: print ('Error with connection. Wait for 1 minute to try again...') # Wait for 30 seconds time.sleep(30) continue
The next step is to remove the missing values from the data by using the .dropna() function from pandas. Finally, the collected data is serialized and saved in two different files using .to_pickle. In this way, we can later load data directly as a pandas DataFrame: # Remove the missing values from dataframe data = data.dropna() # Save data data.iloc[:, 0].to_pickle('data/nasdaq100_index_6y.pkl') data.iloc[:, 1:].to_pickle('data/nasdaq100_6y.pkl')
[ 148 ]
Index Replication by Autoencoders
Chapter 4
Note that we will illustrate other ways to download historical price data from public sources later in this book, for example in Chapter 6, Trading Rule Identification by CNN. Now that we have the stock data, let's move on to the implementation of the AE to replicate the index.
Implementing a vanilla AE In this section, we will illustrate the necessary steps to implement a vanilla AE to minimize the reconstruction error. They include data exploration and preparation, creating and fitting the model, and finally evaluating the model.
Data exploration and preparation We will start with the data exploration step, which consists of summarizing the main characteristics of our dataset, including the structure of the data, the distribution of values, and the presence of some patterns. The goal is to obtain some basic understanding of our data. Let's analyze what time series in the dataset look like. We first load the data gathered in the previous section and then print the array dimensions by using the NumPy .shape attribute, as in the following code lines: import numpy as np # Load data index_data = pd.read_pickle('data/nasdaq100_index_6y.pkl') stocks_data = pd.read_pickle('data/nasdaq100_6y.pkl') stocks_symbols = data.columns.values # Shape of stocks data print("Stocks data (time series) shape: {shape}".format(shape=data_assets.shape)) # Shape of index data print("Index data (time series) shape: {shape}".format(shape=data_index.shape))
We get the following output: Stocks data (time series) shape: (1510, 98) Index data (time series) shape: (1510,)
[ 149 ]
Index Replication by Autoencoders
Chapter 4
This means that stocks data has a shape of 1,510 x 98 since there are 98 series of 1,510 timesteps corresponding with the assets forming the NASDAQ100 index. Similarly, the index data is a series of 1,510 samples along one single dimension. It can also be useful to print the first rows of the stocks_data DataFrame, to quickly check whether the values are of the right type. This is easily done by applying the .head() method to the object, as follows: # Show headlines stocks_data.head()
The following screenshot shows the first five lines of some stocks:
Data preparation is a necessary step before training any neural network. It usually includes data splitting and scaling. First, we need to split our data into two sets: one for training the model and another for backtesting purposes. The following code snippet splits the data into train and test datasets with the common proportion of 80/20: # Split data n_train = int(data_assets.shape[0]*0.8) # Stocks data X_train = data_assets.values[:n_train, :] X_test = data_assets.values[n_train:, :] # Index data index_train = data_index[:n_train] index_test = data_index[n_train:]
[ 150 ]
Index Replication by Autoencoders
Chapter 4
Next, we scale our data to adjust each feature to a given range and hence make the convergence of the neural network model easier since the network parameters are initialized. Input features should be small values, usually in the [0,1] range, so we will normalize our time series to such range. (MinMax) normalization consists of rescaling the data from the original range so that all values of each feature are within a given range, [a,b]:
The desired range of transformation depends on the nature of data, but typical ranges are [0,1] and [1,1]. Data can be normalized by using the MinMaxScaler object from the scikitlearn library, which scales data to the [0,1] range by default. Good practice on the usage of MinMaxScaler (and other preprocessing techniques) requires first fitting the object using the training data and then applying the transformation to both the training and testing datasets. In our case, we are interested in backtesting offline, so we will fit the scaler also with the testing data. In the following code, after creating the MinMaxScaler() object, we perform the fit and transformation in a single step using the fit_transform() function. Note that we use the np.newaxis expression to convert index_train and index_test arrays into column vectors, by adding an axis along the second dimension, as required by fit_transform(): from sklearn.preprocessing import MinMaxScaler # Normalize data # Stocks data scaler = MinMaxScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.fit_transform(X_test) # Index data scaler_index = MinMaxScaler() index_train = scaler_index.fit_transform(index_train[:,np.newaxis]) index_test = scaler_index.fit_transform(index_test[:, np.newaxis])
It is worth noting that we do not have defined the target data. This is because the AE is trained to reconstruct the input from the latent space in an unsupervised manner. Therefore, the target data is the same as the input data.
[ 151 ]
Index Replication by Autoencoders
Chapter 4
Creating and fitting the model Now is the time to implement the vanilla AE using Keras. The vanilla AE is the simplest form of AE, which is a neural network with one hidden layer. The model will take as input the price of every stock at each time step and will give as output the same values (an approximation). As we have seen in Chapter 2, Designing Neural Network Architectures, an autoencoder is an unsupervised learning model that consists of two parts, the encoder and the decoder. The encoder maps the input data to a lowerdimensional space to extract the most robust features from data, and then the decoder interprets these features and uses them to reconstruct the original input. In this example, the encoder will learn a latent lowerdimensional representation from the stock's price series while the decoder will try to reconstruct them from the encoded features. First of all, we must import the libraries needed to create our model: from keras.models import Model from keras.layers import Input, Dense
Next, let's define some network and training hyperparameters. They include the input shape (n_inputs), which is equal to the input dimension; the batch size (batch_size), which we set to 1; and the number of epochs (epochs), which we set to 50 for all of the experiments in this chapter: # Network hyperparameters n_inputs = X_train.shape[1] # Training hyperparameters epochs = 50 batch_size = 1
Now, we can create the autoencoder structure. As we have seen in Chapter 1, Deep Learning for Trading 101, defining a vanilla AE is pretty simple with Keras since it is composed of two Dense layers. The code layer is usually small to force the autoencoder to learn more representative features, while the output has the same size as the input layer. The following code snippet instantiates the model by using the Keras functional API. We first define the layers, which are callable by a tensor and return a tensor, and then define Model given the input and output tensors. Note that we define the autoencoder in this way in case you want to get access to the latent space. We also define the encoder model to get access to the encoded representation: # Create the model input = Input(shape=(n_inputs,)) # Encoder encoded = Dense(4, input_shape=(n_inputs,), activation='relu')(input)
[ 152 ]
Index Replication by Autoencoders
Chapter 4
# Decoder decoded = Dense(n_inputs, activation='sigmoid')(encoded) # Encoder model model = Model(input, encoded) # Autoencoder model model = Model(input, decoded)
In the preceding code snippet, the code layer is made of 4 neurons and uses ReLU as the activation function. The use of the ReLU function here helps a faster training since it is cheap to compute. Instead, the output layer uses the sigmoid function since the output values fall in the [0,1] range. Let's take a look at the network structure. We can obtain a summary representation of the model by using the .summary() method: model.summary()
This outputs the following:
The number of parameters of the hidden layer is 396 because every hidden unit has 98 input weights and one weightrelated to the bias, hence every unit is associated with 99 network parameters for a total of 393. Likewise, every neuron of the output layer is associated with 5 parameters amounting to 490 parameters. After defining the architecture, we must configure the model for training. This is done by using the .compile() method. We are interested in using the MSE as the loss function to measure the difference between the reconstructed input and the original one and the Adam optimizer. To do this, we pass loss='mse' and optimizer='adam' as arguments to the .compile() method, as follows: # Compile the model model.compile(loss='mse', optimizer='adam')
[ 153 ]
Index Replication by Autoencoders
Chapter 4
Finally, we can train the autoencoder by applying the .fit() method to the model. We use X_train for both the input and the target data, as we are interested in reconstructing the original data. We set shuffle=True to randomly shuffle the training samples before each epoch, and hence speed up the convergence of the network and avoid overfitting. You can set verbose=0 to impose the silent mode: # Fit the model history = model.fit(X_train, \ X_train, \ epochs=epochs, \ batch_size=batch_size, \ shuffle=True, \ verbose=0 )
It may be interesting to visualize the model performance over time during training. This is possible with Keras since the .fit() method returns a History object, whose .history attribute is a dictionary recording training metrics for each epoch. Depending on the model configuration, they include the loss and accuracy for training and validation sets. You can list the metrics stored in History by using the following code snippet: # List data recorded in history print(history.history.keys())
For the preceding trained model, you will get the following listing: ['loss']
Now we can use the data recorded in History to generate the training loss plot. Note that you need to import the matplotlib.pyplot library, as follows: import matplotlib.pyplot as plt # Visualize training loss plt.figure() plt.plot(history.history['loss']) plt.ylabel('Loss') plt.xlabel('Epoch') plt.show()
[ 154 ]
Index Replication by Autoencoders
Chapter 4
The resulting plot is provided as follows:
From the loss curve in the preceding, we can see that the model rapidly converges over the first 7 epochs and then the loss decreases slowly until reaching a plateau, indicating the convergence of the model. If you want to generate the same network parameters (and any random number) every time for the reproducibility of your experiment, you can fix the random seed at the beginning of your script with the following lines of code: seed = 37 # ANY int number np.random.seed(seed)
Evaluating the model After training any DL model, we usually evaluate it by using the testing data. Keras allows us to easily evaluate the model by using the .evaluate() method, which returns the metrics. The following snippet of code prints the loss score using both training and testing sets. We also need to pass the batch size to the method since the computation is done in batches: # Evaluate model score_train = model.evaluate(X_train, X_train, batch_size=batch_size) score_test = model.evaluate(X_test, X_test, batch_size=batch_size) print('Training MSE: %.8f' %score_train) print('Training MSE: %.8f' %score_test)
[ 155 ]
Index Replication by Autoencoders
Chapter 4
Here is the output: Training MSE: 0.00553699 Testing MSE: 0.07512729
The training MSE is very small as expected from the training loss plot. The testing MSE is higher as the testing data is new for the network. Try to run the same experiment but using more units in the code layer. Are you able to achieve better performance? Be careful with using too many neurons as you can overfit the model. In reality, here, we are not interested in the model performance in terms of MSE, but the ability to replicate the index by using the autoencoder. Let's continue with the following section!
Replicating an index by using an AE Now it is time to replicate the NASDAQ100 index through a portfolio containing a small group of stocks. The basic idea is to identify the components that better represent the aggregate information of all of the stocks composing the index. We use an autoencoder to identify the group of stocks forming the tracked index inspired by Heaton, Polson, and Witte (2016). The AE first encodes all time series passing through a latent space and then reconstructs them from the reduced information. The resulting series will be more close to the original ones as they contain more communal information and they exert more influence in the index. Therefore, the stocks with lower reconstruction error will be chosen to comprise the portfolio. We will use the MSE as metric to evaluate the quality of the reconstruction. First, we need to obtain the reconstruction of all of the stocks in the training data. This can be done by using the .predict() method of the model object. Then, we calculate the MSE and accordingly sort the stocks in increasing order by using the .argsort() function from NumPy to get the corresponding indices. Let's code it: # Obtain reconstruction of the stocks X_train_pred = model.predict(X_train) # Reconstruction error error = np.mean(np.abs(X_train  X_train_pred)**2, axis=0)
[ 156 ]
Index Replication by Autoencoders
Chapter 4
# Sort stocks ind = np.argsort(error) sort_error = error[ind] sort_assets_names = assets_names[ind]
The following piece of code generates a horizontal barplot to visualize the 20 stocks with a lower MSE. We use the .barh() function from matplotlib.pyplot, which takes as arguments the y coordinates of the bars and their widths: # Barplot plt.figure() plt.barh(2*np.arange(len(error[:20])), error[ind[:20]], tick_label=assets_names[ind[:20]]) plt.xlabel('MSE') plt.show()
This generates the following barplot:
It is worth noting that three out of the four stocks with a minimum MSE belong to the same sector, as expected, corresponding with the semiconductors industry. They are Lam Research Corporation (LRCX), Microchip Technology Inc. (MCHP), and Nvidia Corporation (NVDA). Now, let's compare some stock with its reconstruction. The following piece of code is aimed at plotting the stock with lower MSE and the reconstruction output by the autoencoder: i=0 plt.figure() plt.plot(X_train[:, ind[i]], label=assets_names[ind[i]] + ' Stock') plt.plot(X_train_pred[:, ind[i]], label=assets_names[ind[i]] + ' AE')
[ 157 ]
Index Replication by Autoencoders
Chapter 4
plt.legend() plt.xlabel('Time (days)') plt.ylabel('Normalized price') plt.show()
We get the following output:
You can also plot, for example, the stock with the higher reconstruction MSE by changing i = 1 in the previous code lines. This is the output:
[ 158 ]
Index Replication by Autoencoders
Chapter 4
Finally, we are ready to create the portfolio with the n stocks with lower reconstruction error. Here, we use only 5 stocks composing the portfolio to enhance the ability to track the index with a very small group of components. You can increase n to create a more diversified portfolio. On the other hand, to create an equalweighted portfolio, we just take the mean value of the selected stocks for each time step. See the following code snippet: # Identify stocks n = 5 portfolio_train = X_train_pred[:, ind[:n]] # Create portfolio tracked_index_insample = np.mean(portfolio_train, axis=1)
Now, let's visualize what the tracked index looks like in comparison with the NASDAQ100 index: # Plot tracked index plt.figure() plt.plot(index_train, label='Nasdaq100 Index') plt.plot(tracked_index_insample, label='Tracked Index') plt.legend() plt.xlabel('Time (days)') plt.ylabel('Normalized price') plt.show()
The resulting plot is the following:
We can see from the preceding diagram that the tracked index that we have created provides a consistent index replication, following the trend of the original index rather well.
[ 159 ]
Index Replication by Autoencoders
Chapter 4
A common measure to determine how the tracked index performs relative to its benchmark index is the correlation coefficient, especially the Pearson correlation coefficient. The Pearson correlation coefficient (PCC) is a statistical measure that calculates the linear relationship between two variables. It varies between 1 and +1, where 1 is perfect negative linear correlation, +1 is perfect positive correlation, and 0 indicates no correlation. We can easily calculate the PCC between the tracked index and the NASDAQ100 using the .corrcoef() function from NumPy, which returns the correlation coefficient matrix of the two variables: # Correlation coefficient corr_train = np.corrcoef(index_train.squeeze(), tracked_index_insample)[0, 1] print('Correlation coefficient (insample): %.8f' %corr_train)
Here is the output: Correlation coefficient (insample): 0.94637850
This means that effectively there is a strong correlation between the movements of both the tracked index and the original index. Up to now, we have made an analysis insample, that is, by using the data belonging to the training period to replicate the index over the same period. What will happen if we try to track the index outofsample by using the testing data? First, we need to pass the test data related to the stocks through the trained autoencoder. Then, we use the n stocks identified with the training data to create the portfolio outofsample: # Make prediction X_test_pred = model.predict(X_test) # Identify stocks portfolio_test = X_test_pred[:, ind[:n]] # Create portfolio tracked_index_outofsample = np.mean(portfolio_test, axis=1)
Now, let's plot the tracked index outofsample related to the NASDAQ100: # Plot tracked index (outofsample) plt.figure() plt.plot(index_test, label='Nasdaq100 Index') plt.plot(tracked_index_outofsample, label='Tracked Index')
[ 160 ]
Index Replication by Autoencoders
Chapter 4
plt.legend() plt.xlabel('Time (days)') plt.ylabel('Normalized price') plt.show()
We get the following output:
In the preceding diagram, we can see how the tracked index follows closely the Nasdaq100 index. Not bad, right? Next, we calculate the corresponding PCC: # Correlation coefficient (outofsample) corr_test = np.corrcoef(index_test.squeeze(), tracked_index_outofsample)[0, 1] print('Correlation coefficient: %.8f' %corr_test)
This is the output: Correlation coefficient (outofsample): 0.86059389
From the preceding results, we can say that even outofsample, we can replicate the index quite well using the autoencoderbased procedure. However, it could be possible to improve the performance of the vanilla AE by exploring some of its variants. We will do this in the next section.
[ 161 ]
Index Replication by Autoencoders
Chapter 4
Exploring some AE variants By using the vanilla AE implemented in the previous section, where the only constraint is the size of the code layer, usually the hidden layer learns just an approximation of Principal Component Analysis (PCA). In this section, we will implement some variants of the vanilla AE that avoid this issue and may improve the performance. They include the following: The denoising AE The sparse AE
The denoising AE Corrupting the input data is one strategy to make the learned features in the latent space more robust. The idea is to train the autoencoder to reconstruct the clean data from its corrupted version. This is known as the denoising autoencoder. How can we corrupt our data? The simplest way is by adding Gaussian noise. The following piece of code generates noisy data by adding random noise from the normal distribution using the numpy.random.normal() function. Note that we clip the corrupted price series to force them to fall in the [0,1] range: # Generate corrupted series by adding gaussian noise noise_factor = 0.05 X_train_noisy = X_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=X_train.shape) X_test_noisy = X_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=X_test.shape) # Clip corrupted data X_train_noisy = np.clip(X_train_noisy, 0., 1.) X_test_noisy = np.clip(X_test_noisy, 0., 1.)
Let's see what the noisy series looks like. The following code snippet creates a subplot showing the original price series of three stocks in the top row and their corrupted versions in the bottom row: # Visualize corrupted data f, axarr = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(15,5)) # Asset 1 axarr[0,0].plot(X_train[:, 0]) axarr[0,0].set_title(assets_names[0])
[ 162 ]
Index Replication by Autoencoders
Chapter 4
axarr[1,0].plot(X_train_noisy[:, 0]) # Asset 2 axarr[0,1].plot(X_train[:, 1]) axarr[0,1].set_title(assets_names[1]) axarr[1,1].plot(X_train_noisy[:, 1]) # Asset 3 axarr[0,2].plot(X_train[:, 2]) axarr[0,2].set_title(assets_names[2]) axarr[1,2].plot(X_train_noisy[:, 2]) plt.show()
Here is what we get:
Now it is time to implement the denoising AE. The architecture is just the same as before; we must change only the fit function, as follows: # Fit the model history = model.fit(X_train_noisy, \ X_train, \ epochs=epochs, \ batch_size=batch_size, \ shuffle=True, \ verbose=0 )
[ 163 ]
Index Replication by Autoencoders
Chapter 4
Next, we evaluate the model: # Evaluate model score_train = model.evaluate(X_train_noisy, X_train, batch_size=batch_size) score_test = model.evaluate(X_train_noisy, X_test, batch_size=batch_size) print('Training MSE: %.8f' %score_train) print('Training MSE: %.8f' %score_test)
We get the following as output: Training MSE: 0.00552607 Testing MSE: 0.06268836
The training MSE is practically the same as using the vanilla AE, but the testing MSE is slightly lower. Predictions are also done by introducing the noisy data into the network: # Obtain reconstruction of the stocks X_train_pred = model.predict(X_train_noisy) X_test_pred = model.predict(X_test_noisy)
Everything else in the code is exactly the same. The following is the barplot with the stocks better reconstructed:
From the preceding plot, we can see that the two stocks with lower MSE match those individuated by the vanilla AE. Also, the 20 stocks with lower MSE in both cases overlap quite well, as expected.
[ 164 ]
Index Replication by Autoencoders
Chapter 4
In the following screenshot, you can see the stocks with lower MSE (left) and higher MSE (right) and their reconstructions:
The following screenshot shows the tracked index insample (left) and outofsample (right):
The PCC between the tracked index and the NASDAQ100 in both cases is as shown in the following: Correlation coefficient (insample): 0.96413310 Correlation coefficient (outofsample): 0.89888135
The correlation is slightly better than using the vanilla AE in both cases. Is it not amazing?
[ 165 ]
Index Replication by Autoencoders
Chapter 4
The sparse AE Another way to facilitate the learning of more interesting and useful features for the autoencoder is by imposing the sparsity constraint on the output of the code layer. The idea is to switch off different connections at each timestep to learn a code representation that minimizes the reconstruction error by using a small combination of code features. To achieve this, a penalty term is added to the optimization objective. In Keras, this can be easily done by adding activity_regularizer to the Dense layer to apply a penalty on layer activity during training, which is included in the loss function. The following snippet of code implements the sparse autoencoder by adding an L1 regularization term with a factor of 10e5. The rest of the code remains as before: # Create the model input = Input(shape=(n_inputs,)) # Encoder encoded = Dense(4, input_shape=(n_inputs,), activation='relu', \ activity_regularizer=regularizers.l1(10e5))(input) # Decoder decoded = Dense(n_inputs, activation='sigmoid')(encoded) # Encoder model model = Model(input, encoded) # Autoencoder model model = Model(input, decoded)
The MSE obtained on the training and testing sets is the following: Training MSE: 0.00540901 Testing MSE: 0.06508816
The error is again slightly lower than in the simplest case of vanilla AE. However, the most interesting difference when applying the sparsity constraint is the mean value of the encoded representations. Let's obtain the code values of the test set by using the encoder model and then compute the mean value of all of them: # Predict code values code_values = encoder.predict(X_test) # Compute mean mean_code = np.mean(code_values, axis=(0, 1)) print('Mean of code values for sparse AE: %.8f' %mean_code)
[ 166 ]
Index Replication by Autoencoders
Chapter 4
This gives the following output: Mean of code values for sparse AE: 0.29900122
Instead, if you do the same for the vanilla AE, you get this: Mean of code values for vanilla AE: 0.60625684
The mean for the sparsity AE is significantly lower. This demonstrates that a large number of code values in the regularized model are equal to 0, as expected. What about the portfolio construction? Here is the barplot with the 20 stocks of NASDAQ100 with a lower reconstruction MSE:
Most of the stocks coincide once again with those identified by the vanilla AE.
[ 167 ]
Index Replication by Autoencoders
Chapter 4
The following screenshot shows the two stocks with lower MSE (left) and higher MSE (right) and their reconstructed versions:
The following screenshot displays the tracked index insample (left) and outofsample (right):
Here are the corresponding PCC values: Correlation coefficient (insample): 0.95151258 Correlation coefficient (outofsample): 0.91977147
[ 168 ]
Index Replication by Autoencoders
Chapter 4
The correlation obtained with the sparse AE is significantly better than using the vanilla AE. Can we further improve it? Let's try using a deep architecture.
Understanding deep AE Up to now, we have implemented a single layer encoder and a single layer decoder, but now it's time to extend this shallow architecture by stacking multiple layers for encoding and decoding. This is known as a Deep Autoencoder (DAE). Building a DAE can bring many advantages. It is known that deep architectures can exponentially reduce the computation cost and the amount of data required for training. Also, the latent representations learned by a DAE are relatively robust and useful compared to a shallow AE. The following piece of code implements a DAE with 4 units in the code layer and 8 units in the intermediate layers. Look at the symmetry between encoder and decoder: # Create the model input = Input(shape=(n_inputs,)) # Encoder encoded = Dense(8, input_shape=(n_inputs,), activation='relu')(input) encoded = Dense(4, activation='relu')(encoded) # Decoder decoded = Dense(8, activation='relu')(encoded) decoded = Dense(n_inputs, activation='sigmoid')(decoded) # Autoencoder model model = Model(input, decoded)
A common practice when implementing a DAE is first to define the code dimension and then use twice the number of units in the intermediate layers. Note that we use the ReLU activation function in all hidden layers of the DAE implemented here. Besides accelerating the training process, the ReLU function helps to avoid the vanishing gradient problem when training deep architectures since it doesn't cause a small derivative.
[ 169 ]
Index Replication by Autoencoders
Chapter 4
The vanishing gradient problem affects the training of deep architectures composed of many hidden layers when using the backpropagation algorithm. In this method, the error in the output is calculated and the gradient of the error for every weight in the network is computed. The weights are accordingly updated. The problem lies in the fact that gradients become smaller as they reach the initial layers. For this reason, the weights of the initial layers will be updated little by little or even not be changed. Therefore, the deep neural network may not converge properly. Let's display the summary of the DAE structure:
We definitely have many more parameters to learn now. They are 1,750 trainable parameters, whereas the shallow AE implemented before consisted of 886 parameters. The training and testing MSE that we achieve with this model are the following: Training MSE: 0.00465250 Testing MSE: 0.08797098
The MSE on the training dataset is lower than for the vanilla AE; instead, the MSE on the testing set is slightly higher. Let's see what happens with the index tracking outofsample.
[ 170 ]
Index Replication by Autoencoders
Chapter 4
The barplot with the stocks with lower reconstruction MSE is shown in the following:
The following screenshot displays the stocks with the lower MSE (left) and higher MSE (right) and their AE reconstructions. They are always the same stocks:
[ 171 ]
Index Replication by Autoencoders
Chapter 4
Here, the tracked index insample (left) and outofsample (right) are shown:
The tracked index seems to be more smooth now and follows the movements of the NASDAQ100 index over the testing period pretty well. We obtain the following PCC values: Correlation coefficient (insample): 0.97687587 Correlation coefficient (outofsample): 0.96098996
We have reached a very high correlation coefficient even during the testing period! This is because of the robustness of the learned latent features by the DEA during training.
Summary In this chapter, we created and trained a vanilla AE to identify the most communal features of the stocks composing the NASDAQ100 index, to replicate such an index. The strategy consists of creating an equalweighted portfolio with those stocks whose reconstruction error after passing through the autoencoder are lower, assuming that they are the most representative stocks of the market index. Also, we implemented some variants of the vanilla AE, including a deep AE.
[ 172 ]
Index Replication by Autoencoders
Chapter 4
First, we saw how to download the historical price series and how to prepare it for the neural network. Then, we created, trained, and evaluated the AE model in Keras. We used the trained AE to construct the tracking portfolio to replicate the NASDAQ100 index. Finally, we have seen how to implement some changes in the previous model to improve performance. These include enriching the input samples with stochastic noise (the denoising AE) and adding the sparsity constraint (the sparse AE). We also implemented a deep autoencoder (DAE) by stacking more encoder and decoder layers resulting in a deep architecture, thereby learning more robust features. The results achieved in this chapter suggest that autoencoding seems to be a robust approach for index replication in all of its variants, with the DAE outperforming the others. In the next chapter, we will investigate how to implement an LSTMbased model for multivariate volatility forecasting.
[ 173 ]
5 Volatility Forecasting by LSTM In this chapter, you will learn how to implement a deep learning architecture for multivariate volatility forecasting based on the Long ShortTerm Memory (LSTM) model, using the Keras framework. We will focus on data preparation, creating and fitting the network, and finally evaluating the model. We will also discuss the importance of choosing the activation function and the loss function correctly. A comparison with the classic recurrent neural network (RNN) and some statistical models is also provided. This chapter will include the following topics: Measuring volatility Loading the data Implementing the LSTM model Improving the model's performance Comparing LSTM with other models
Measuring volatility In finance, volatility is one of the most important factors to take into consideration to make appropriate decisions. Volatility is a measure of the variation of prices for a given stock or a market index over a period of time. Low volatility indicates stock prices that are rather stable, while high volatility is associated with wild price fluctuations associated with a risky market. Volatility is particularly significant in several financial activities such as risk and portfolio management or derivative pricing. Hence, predicting volatility is very useful for traders and investors to make appropriate risk adjustment decisions.
Volatility Forecasting by LSTM
Chapter 5
There are numerous techniques for predicting volatility, including the Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model and the Multiplicative Error Model (MEM). These methods are linear, univariate, and parametric models that make certain assumptions to reduce the parameter space, but they also reduce the profitability of the model. Multivariate, nonparametric techniques are potentially more suitable because they use common features and volatility spillovers without making any assumptions. Feedforward neural networks are a good option to study nonlinear dependencies and trends from multiple inputs, but they require fixed lag inputs to make a prediction, the same as traditional forecasting methods. RNNs, however, can learn temporal dependencies of variable length series by splitting them into smaller sequences. In particular, the LSTM network is ideal for identifying patterns when the input data consists of time sequences because it can keep the memory about the internal state over long periods of time, as we saw in Chapter 2, Designing Neural Network Architectures.
Types of volatility Volatility refers to the degree of fluctuation of a price. It is not directly observable and there are different definitions in the financial domain that are used to measure it. We can distinguish between the following types of volatility: Historical volatility Implied volatility Volatility index Intraday volatility Realized volatility
Historical volatility Historical volatility measures the past price changes of an underlying financial instrument over a given period of time. It is usually defined as the standard deviation of returns, which is obtained by calculating the variance as follows:
Here, stands for the expected value, represents the returns over a given time period, and is the mean price in the same period.
[ 175 ]
Volatility Forecasting by LSTM
Chapter 5
Historical volatility may be used to predict future price movements based on previous behavior, but it actually does not provide insights regarding the future trend or direction of prices.
Implied volatility Implied volatility isn't calculated from the historical prices of the stock but rather estimates the future volatility by looking at the market price of the options. Whereas historic volatility is static for a fixed given period of time, implied volatility varies for a stock based on different options strike prices. Implied volatility is calculated by applying the BlackScholes option pricing model, which is a partial differential equation that describes the price of an option over time, and solving it for the value of volatility. There are several approaches to calculate the implied volatility, with the iterative search being the most straightforward method. Implied volatility is a key parameter in option pricing since it provides a forwardlooking estimation on possible future price fluctuations.
Volatility index The volatility index is a measure of volatility applied to a market index or its exchangetraded fund equivalent. There are several volatility indexes quoted in financial markets, with the Chicago Board Options Exchange (CBOE) Volatility Index (VIX) being the most popular one. The volatility index can be calculated as the weighted average of the implied volatilities for several series of options related to a certain index. Traders use this index as an indicator of the investor sentiment to identify if there is too much optimism or fear in the market, and hence possible reversals.
Intraday volatility Intraday volatility makes use of highfrequency asset prices so it represents the market fluctuations during a trading day. Intraday volatility provides much more accurate estimations of variance since it uses more observation.
[ 176 ]
Volatility Forecasting by LSTM
Chapter 5
Realized volatility Realized volatility also uses intraday information. It's based on the realized variance and introduced by BarndorffNielsen and Shephard (2001). Over an interval of time of a length of , the realized variance can be defined as the sum of squared intraday changes over a day:
Here, represents the stock logprices and is the number of intraday observations during a day. The realized volatility is the square root of the realized variance. It provides an efficient measure of volatility since it takes into account all of the transactions in a given day. We have seen different measures of volatility, some of which can be used to predict future volatility trends. The following section introduces the dataset that we will use in the experiments of this chapter.
Loading the data The dataset that we are going to use was first used by Hansen et al. (2010). It includes 28 assets from the Dow Jones Industrial Average (DJIA). It also includes an exchangetraded fund, SPY, that tracks the S&P 500 index. The data ranges from January 1, 2002, to August 31, 2008, and is made up of 1,640 samples. For each asset, data concerning the openclose returns and the realized volatility is available. The realized measure of volatility that we used is given by a realized kernel that was introduced by BarndorffNielsen et al. (2008). The realized kernel yields a more robust estimation of volatility even in the presence of noise. Also, it was implemented as proposed by BarndorffNielsen et al. (2011) to guarantee positive values. This dataset is available in this book's repository at the following link: https://github. com/PacktPublishing/HandsOnDeepLearningforFinance. It can be loaded easily using the pandas library. We are interested in the returns and volatility series of each asset, which correspond to the third and fourth columns in the dataset respectively.
[ 177 ]
Volatility Forecasting by LSTM
Chapter 5
Here, you have a screenshot of the dataset related to one of the stocks:
The code to load the dataset and plot the first asset is listed as follows: 1. First, we will import the required packages and define some variables: import import import import
numpy os pandas as pd numpy as np matplotlib.pyplot as plt
# Variable definition data_folder = 'Data/' # Folder containing the dataset n_days = 1640
2. Now, we will load the dataset stepbystep: # Preallocate the array dataset = np.empty((n_days, 0)) # Create list to save assets names assets = [] for f in sorted(os.listdir(data_folder)): # Save assets names assets.append(f.replace('.csv', '')) # Load data asset = pd.read_csv(data_folder + f, sep=',', usecols=[2, 3], engine='python')
[ 178 ]
Volatility Forecasting by LSTM
Chapter 5
asset = asset.values[:n_days] # Ensure all data is float asset = asset.astype('float32') dataset = np.append(dataset, asset, axis=1)
3. Let's now plot the returns and volatility related to the first assets in our dataset: # Visualize returns and volatility of the first asset i = 0 plt.plot(dataset[:, 0], label='returns') plt.plot(dataset[:, 1], label='volatility') plt.legend() plt.title(assets[0]) plt.xlabel('Time (days)') plt.show()
The following graph shows the openclose returns and realized volatility, corresponding to the first asset of our dataset:
We can see from the preceding diagram how the volatility only takes positive values.
[ 179 ]
Volatility Forecasting by LSTM
Chapter 5
Implementing the LSTM model In this section, we discuss the steps required to forecast volatility based on the LSTM network by using the dataset featured earlier. These steps include data preparation, creating and fitting the LSTM model, and finally evaluating it.
Data preparation Before defining and training the model, we need to prepare the dataset. This includes the standardization of time series data, setting up a supervised learning problem, and splitting the data into training and testing datasets. First, we standardize the time series to make sure the different input features are on the same scale. Standardization consists of transforming a variable to have zero mean and unit variance by removing the mean and dividing it by the standard deviation. This process generates standard scores, which each represent the number of standard deviations that a specific observation is above or below the mean. Since we expect the returns to have zero mean in the long term and the volatility to always be positive, we standardize both the returns and the volatilities by dividing them directly by the positive square root of the second raw moment. The raw moments are expressed by corresponding order.
, where
is the
The resulting values are then further divided by a factor of two to get at least 95% of samples within the range [1,+1]. This is necessary when using the default hyperbolic tangent (tanh) activation function in the LSTM units to avoid saturation and the possible nonconvergence of the model. The following code calculates the second raw moment for each input feature of the dataset and then scales the features, as explained previously: # Normalize data factor = 2 # Calculate second raw moment M2 = np.mean(dataset ** 2, axis=0) ** (1/2)
[ 180 ]
Volatility Forecasting by LSTM
Chapter 5
# Apply scaling dataset_norm = (1/factor) * (dataset / M2)
The next step consists of transforming our dataset to get it ready for a supervised learning problem. We use a function that takes the dataset and the lookback as arguments and returns the input and target data for the LSTM model. The lookback is a hyperparameter that represents the number of previous time steps used to predict the next one. The function that we use creates sequences of a length that is equal to the lookback by moving one step ahead each time. These sequences correspond to the input data of the LSTM. It also associates each sequence with the observation at the next timestep, which represents the target of the model. The following code defines the function explained previously and applies it to our dataset by setting the lookback to 20 days, which is a common choice for this kind of analysis since 20 days are equivalent to one month. It is worth noting that we add to the target only the columns related to the volatility series, taking away those corresponding to the returns. Then, we define the variables representing the number of input features (n_features) and the number of considered assets (n_assets) that will be used later: # Convert series to supervised learning problem def create_dataset(dataset, look_back=1): data_x, data_y = [], [] for i in range(len(dataset)  look_back): # Create sequence of length equal to look_back x = dataset[i:(i + look_back), :] data_x.append(x) # Take just the volatility for the target data_y.append(dataset[i + look_back, 1::2]) return np.array(data_x), np.array(data_y) look_back = 20 X, y = create_dataset(dataset_norm, look_back) # Declare variables n_features = dataset.shape[1] n_assets = y.shape[1]
[ 181 ]
Volatility Forecasting by LSTM
Chapter 5
Finally, we split the dataset into the training set and the testing set. We will use the data that corresponds to the initial 300 days to fit the model, then we will evaluate the trained model on the remaining days. The following code splits the dataset and then reshapes the input features so that they have the 3D tensor shape required by the LSTM: # Split dataset training_days = 300 X_train, X_test = X[:training_days], X[training_days:] y_train, y_test = y[:training_days], y[training_days:] # Prepare the 3D input vector for the LSTM X_train = np.reshape(X_train, (1, look_back, n_features)) X_test = np.reshape(X_test, (1, look_back, n_features))
Recurrent layers in Keras take a 3D tensor with the shape, (n_samples, timesteps, input_dim), as an input.
The data prepared here is now ready to be used by the LSTM model.
Creating and fitting the model It's time to create and fit the multivariate model by training the architecture over all of the assets. As an input, the model takes the openclose returns and volatilities for a given past window. This is a sliding window containing past sequence values that have a fixed size expressed by the look_back hyperparameter. As an output, it provides the next onestep volatility, which is the volatility of the following day. The network consists of a first LSTM hidden layer made of 58 units and a fully connected (dense) output layer with 29 neurons, according to the input and output dimensions respectively. We use a stateful LSTM, which can compute and keep the internal state along with the whole dataset. In Keras, a stateless LSTM resets the internal state at the end of each batch of samples during the training phase. A stateful LSTM preserves the state across batches and must be manually reset at the end of each epoch.
[ 182 ]
Volatility Forecasting by LSTM
Chapter 5
First of all, we import the necessary libraries to define the model: from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout from keras import regularizers
We set the batch size to 1 since we will make onestepahead predictions on the testing set: batch_size = 1
Then, we define a sequential model by calling the .Sequential() method. After this, we can add layers to the model via the .add() method: # Create the model model = Sequential()
LSTM layers can be created using the Keras LSTM() class. The LSTM layer constructor takes as arguments the number of units and the input shape (input_shape=(timesteps, input_dim)) because it is the first layer of our network. A stateful LSTM requires to specify the batch size since it keeps information from one batch to the next and then it is necessary to know the position of the samples within the batch. To enable statefulness, we must specify stateful=True in the layer constructor: model.add(LSTM(58, input_shape=(look_back, n_features),\ batch_size=batch_size,\ stateful=True,\ activity_regularizer=regularizers.l1_l2(),\ recurrent_regularizer=regularizers.l1_l2()))
Regularization is important to deal with the undesired overfitting. We have included in the preceding implementation L1 and L2 regularization terms applied to the recurrent weights matrix (recurrent_regularizer) and the output (activity_regularizer) of the LSTM by using the regularizers.l1_l2() method. Then, we add a dropout layer to further reduce the overfitting. The Dropout() constructor takes as input the dropout rate, that is, the fraction of inputs set to 0 during training: model.add(Dropout(0.2))
The arguments of the output dense layer are the number of units and the nonlinear activation function. We use the sigmoid in this first example by setting activation='sigmoid': model.add(Dense(n_assets, activation='sigmoid'))
[ 183 ]
Volatility Forecasting by LSTM
Chapter 5
The next step is to compile the model by using the .compile() method. We use the Mean Square Error (MSE) as a loss function. We also use the RMSProp optimizer, which is an adaptive learning rate method proposed by Hinton and recommended for RNNs. More information about this topic can be found at the following link: https://keras.io/ optimizers/. The necessary code to compile the LSTM model is listed as follows: # Compile the LSTM model model.compile(loss='mse', optimizer='rmsprop')
We are now ready to train the network by applying the .fit() method to the model. We use 300 epochs for fitting, which is not to be confused with the 300 training days! Training a stateful LSTM requires resetting the model state after each epoch by calling .reset_states(). We can do this inside a loop by setting the hyperparameter of the epoch to 1. A stateful LSTM also requires us to specify shuffle=False when calling .fit() since it is important to preserve the temporal order of the samples. The batch_size hyperparameter is needed to indicate the number of samples required for the gradient update. The following code shows how to train the stateful LSTM model: num_epochs = 300 # Fit the model for i in range(num_epochs): model.fit(X_train,\ # input data Y_train,\ # target data batch_size=batch_size,\ shuffle=False,\ epochs=1,\ verbose=0) # Verbosity mode 0: silent model.reset_states()
After a few minutes, our model will be trained. Once we have our LSTM model trained, it's time to evaluate it. How will it perform with new, unseen data?
[ 184 ]
Volatility Forecasting by LSTM
Chapter 5
Evaluating the model After training our model, we can now make predictions on the test dataset for an outofsample evaluation. We apply a rolling forecast window to forecast the volatility every time onestepahead, using the new available information to predict the volatility of the next day, as we may often want to do in real life. Predictions in Keras are made using the .predict() method, which takes the test set and the batch size as input. The following code makes the predictions on our test dataset, as explained previously: # Make a prediction (rolling test window) y_pred = np.empty((0, n_assets)) for i in range(len(X_test)): X_i = X_test[i].reshape(1, look_back, n_features) predicted_output = model.predict(X_i, batch_size=batch_size) # Reshape prediction to save into array predicted_output = predicted_output.reshape(1,n_assets) y_pred = np.append(y_pred, predicted_output, axis=0) y_pred = predicted_output.reshape(1, n_assets) y_true = y_test.reshape(1, n_assets)
Before calculating the error scores, we need to invert the standardization applied during the data preparation step to return the time series to their original scale. Remember that we now only have to revert the transformation on the volatility series, which is our target. Hence, we modify the M2 variable to preserve the volatility moments and take away those corresponding to the openclose returns. We create a function to carry out this transformation, the arguments of which are the array to rescale, the moment array, and the multiplicative factor of the moment, as shown in the following code: # Invert scaling def invert_standardization(data, M2, factor): # Consider just volatility series M2 = M2[1::2] y_pred = factor * y_pred * M2 return y_pred # Apply inversion y_pred = invert_standardization(y_pred, M2, factor) y_true = invert_standardization(y_true, M2, factor)
[ 185 ]
Volatility Forecasting by LSTM
Chapter 5
The performance metrics that we use depends on the problem that we are studying. In this case, we are going to use the MSE and the QLIKE functions, which are widely used to compare volatility. These are also robust to noise in the volatility proxy, as suggested by Patton (2011). Given a Y vector of N predictions and the Y vector of observations, the MSE and QLIKE functions are defined as follows:
The following code defines a function to calculate the preceding measures with respect to the predicted and expected volatilities. This function returns the average for each stock in the dataset and saves the results point by point into a CSV file for further comparison. We can save the results by using the .savetxt() function from the NumPy library: # Calculate MSE and QLIKE def evaluate(y_true, y_pred, folder): mse = [] qlike = [] for i in range(0, 29): mse_i = (y_true[:, i]  y_pred[:, i]) ** 2 qlike_i = np.log(y_pred[:, i]) + (y_true[:, i] /
y_pred[:, i])
# save results (point by point) results = np.array([mse_i, qlike_i]).transpose() np.savetxt(folder + assets[i] + '.csv', results, delimiter=',', \ header='MSE, QLIKE', fmt='%10.5f', comments='') mse.append(np.mean(mse_i, axis=0)) qlike.append(np.mean(qlike_i, axis=0)) return mse, qlike # Apply function folder = '1BASE' mse, qlike = evaluate(y_true, y_pred) # save results results = np.array([mse, qlike]).transpose()
[ 186 ]
Volatility Forecasting by LSTM
Chapter 5
np.savetxt('1.csv', results, delimiter=',', header='MSE,QLIKE', fmt='%10.5f',\ comments='')
We can now have a look at the results of our experiment. We can summarize them using the .describe() function from pandas. This provides descriptive statistics that summarize the central tendency, dispersion, and shape of the distribution of our dataset. This is done by using the following code: df = pd.DataFrame({'MSE': mse, 'QLIKE': qlike}) print(df.describe())
Results of our first experiment are summarized as follows: out[1]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 9.722069 1.642069 11.882342 0.421446 0.630000 0.600000 2.240000 1.420000 6.240000 1.690000 12.010000 1.910000 58.970000 2.720000
The MSE is too high and is not consistent between the different stocks, as shown by its higher standard deviation in the summary. However, the QLIKE seems to be a more stable measure. Repeat the experiment several times and then try to come to a conclusion. How does random initialization affect the LSTM model?
Improving the model's performance The previous network configuration does not lead to very accurate predictions. In this section, we will try different strategies in an attempt to improve the quality of the predictions.
[ 187 ]
Volatility Forecasting by LSTM
Chapter 5
Online learning Up until now, we have used the first 300 days of data to train the network and then predictions have been made using a rolling forecast window onestepahead without the fitted model being updated. This is what we call offline learning. A more convenient method is to update the model as new observations become available. These could be either returns or realized volatility. This means reexecuting the training of the model at the end of each day before using it to make a forecast for the next day. This can be regarded as a kind of transfer learning, more details of which we will cover in Chapter 10, Chart Visual Analysis by Transfer Learning. The purpose of transfer learning is to adjust the network parameters to identify different patterns in price movements. In our example, after the initial training over 300 days along 300 epochs, we will carry out an additional training phase using the current lookback window along a further 20 epochs. We can do this by including both the model (re)fitting and the prediction inside the oneday moving ahead loop, as shown in the following code snippet: 1. First, we initialize empty NumPy arrays to store the true and predicted sequences daybyday inside the loop: # Create empty arrays y_pred = np.empty((0, n_assets)) y_true = np.empty((0, n_assets))
2. For the initial training, we will use the first 300 days to fit the LSTM model during 300 epochs. The output will be the volatility of the next day: # Train and evaluate the model (online learning) for j in range(training_days  look_back + 1, X.shape[0]): if j == (training_days  look_back + 1): # First training days for training X_train = X[:j] y_train = y[:j] # Next day for forecasting X_test = X[j].reshape(1, look_back, n_features) # Ensure the correct shape for LSTM X_test = np.tile(X_test, (batch_size, 1, 1)) y_test = np.tile(y[j], (batch_size, 1)) # Training epochs epochs = 300
[ 188 ]
Volatility Forecasting by LSTM
Chapter 5
3. On successive days, we will move the sliding window oneday ahead and retrain the network for 20 epochs: else: # Available data to refine network state X_train = X_test y_train = y_test # Ensure X_test = X_test = y_test =
the correct shape for LSTM X[j].reshape(1, look_back, n_features) np.tile(X_test, (batch_size, 1, 1)) np.tile(y[j], (batch_size, 1))
# Epochs for updating epochs = 20
4. Training is done in a stateful mode, as shown in the following code block: # Fit the model for i in range(epochs): model.fit(X_train,\ y_train,\ epochs=1,\ batch_size=batch_size,\ verbose=0,\ shuffle=False) model.reset_states()
5. After training the model, we can make predictions and store the day of our interest, that is, the first element of the output sequence: # Make predictions predicted_output = model.predict(X_test, batch_size=batch_size) predicted_output = predicted_output[0].reshape(1, n_assets) true_output = y_test[0].reshape(1, n_assets) # Save current prediction into an array y_pred.append(predicted_output.reshape(1)) y_true.append(true_output.reshape(1))
[ 189 ]
Volatility Forecasting by LSTM
Chapter 5
The results of this experiment are as follows: out[2]: count mean std min 25% 50% 75% max
MSE 29.000000 4.419655 7.408755 0.210000 0.830000 1.570000 4.260000 36.950000
QLIKE 29.000000 1.349655 0.396966 0.290000 1.100000 1.360000 1.570000 2.260000
These results look much better. The MSE is lower across all statistics. The maximum value of the MSE is still high, however, which suggests the model performed badly for a few stocks. The QLIKE is slightly better than in the previous case. Try using a different number of epochs to update the network. When does it lead to the model overfitting? What about underfitting?
Stacking layers Up until now, our network had consisted of a single hidden LSTM layer. This means that the LSTM tries to identify significant patterns directly on the data, without encoding the input features first. This is the case for shallow neural networks. We can obtain a deep architecture by stacking the LSTM layers. A deep network can enrich the intermediate representation, leading to the ability to learn more abstract patterns. Keras allows us to use stacking layers easily with the model's .add() operation. Each LSTM layer outputs a single value for each LSTM cell in the layer, corresponding to the last timestep of the sequence. When stacking LSTM layers, the second layer needs the full sequence related to each timestep as an input. This can be done by setting return_sequences=True on the first LSTM layer. The following code defines a twolayer stacked LSTM made up of 58 and 29 units respectively. The output of the second LSTM is given as input to the output dense layer: # Create the model model = Sequential() model.add(LSTM(58,\ input_shape=(look_back, n_features),\
[ 190 ]
Volatility Forecasting by LSTM
Chapter 5
batch_size=batch_size,\ return_sequences=True,\ stateful=True,\ activity_regularizer=regularizers.l1_l2(),\ recurrent_regularizer=regularizers.l1_l2())) model.add(Dropout(0.2)) model.add(LSTM(29,\ return_sequences=False,\ stateful=True,\ activity_regularizer=regularizers.l1_l2(),\ recurrent_regularizer=regularizers.l1_l2())) model.add(Dense(n_assets, activation='sigmoid'))
Running the previous model with the same training and testing sets used before, we will get the following results: out[3]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 3.751034 1.298966 6.603842 0.395197 0.170000 0.220000 0.660000 1.070000 1.200000 1.330000 3.390000 1.470000 33.240000 2.200000
As can be seen in the preceding results, stacked layers have allowed us to further improve the performance for both the MSE and QLIKE.
Tuning the hyperparameters The maximum error, however, is still too high. A further improvement we could make would be to tune the hyperparameters. We will focus on the activation function of the output layer and the loss function, which are related to the structure of the network and the training method respectively.
[ 191 ]
Volatility Forecasting by LSTM
Chapter 5
To choose a more suitable activation function, we should think about the nature of our problem and the expected output. Up until now, we have used the sigmoid function, which is convenient for binary predictions or predicting probabilities since it ranges from 0 to 1. In a forecasting model, however, we should use a linear function because the values are unbounded. In our case, we are predicting volatility, which always produces positive values, so the activation function should not provide any negative values. One possibility is to use the Rectified Linear Unit (ReLU) function, but the issue is that no error gradient is passed back into the network if the output is saturated. This may decrease the ability of the model to fit properly from the data. As an alternative, the softplus function may be a better choice. By changing the activation function of the output layer to the softplus function (activation='softplus'), we obtain the following results: out[4]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 2.940345 1.297931 4.232452 0.396911 0.230000 0.220000 0.690000 1.070000 1.110000 1.330000 2.950000 1.480000 20.710000 2.190000
We have obtained a great improvement in terms of the MSE with respect to the initial configuration. The optimization criteria that we have used up until now has been the MSE. Why not use the QLIKE as a loss function? This is particularly suitable for volatility forecasting. We can do this defining a custom loss function using the Keras backend. The following code implements the QLIKE function that will be used during the training of the model: from keras import backend as K def qlike(y_true, y_pred): return K.mean((K.log(y_pred) + (y_true / y_pred)), axis=1)
The only thing that needs to be modified in our code is the loss parameter when we compile the model, as shown in the following: # Compile the model model.compile(loss=qlike, optimizer='rmsprop')
[ 192 ]
Volatility Forecasting by LSTM
Chapter 5
The evaluation metrics obtained with this final configuration of the model are as follows: out[5]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 2.716897 1.289655 4.125356 0.394067 0.190000 0.220000 0.720000 1.070000 1.110000 1.330000 2.420000 1.460000 20.880000 2.170000
From the preceding results, we can conclude that choosing the correct network hyperparameters is the key to a good performance.
Visualizing results It may be useful to compare the performance of the different model versions that we have seen so far. Let's generate a boxplot using the MSE of each asset over the five configurations. This can be easily done by using the .boxplot() function from the Matplotlib library. We read the .csv files with the pandas .read_csv() method. In descriptive statistics, a boxplot is a graphic that displays the distribution of numerical data through their statistics, including the minimum, the first quartile, the median, the third quartile, and the maximum. The following code snippet loads the files containing the results of the preceding experiments and depicts the corresponding boxplot: # Load data files = range(1, 6) mse_all = np.empty((29, 0)) for f in files: mse_f = pd.read_csv(f + '.csv', sep=',', usecols=[0], engine='python') mse_all = np.append(mse_all, mse_f.values, axis=1) # Generate boxplot plt.boxplot(mse_all, labels=files, positions=np.arange(0, len(files)*3, 3)) plt.xlabel('Experiments') plt.ylabel('MSE') plt.show()
[ 193 ]
Volatility Forecasting by LSTM
Chapter 5
The following diagram shows the boxplot of the MSE measure distributions related to all of the assets in our dataset and obtained in the previous experiments:
The preceding boxplot shows how the MSE increasingly decreases as we improve the initial model. The predictions also become more consistent among the assets belonging to the dataset, except for one of the stocks, which should be analyzed independently.
Comparing LSTM with other models In this section, we will compare the proposed deep learning model based on LSTM with a multivariate RNN and the classic GARCH method, which is a widely diffused univariate parametric model for predicting the variance of a time series dataset.
RNN model We will use the Elman network, which is a simple recurrent network. This architecture stores the previous values of the hidden units and can keep a sort of internal state, namely a memory, but only from the most recent past. We will make a comparison with this model to justify the use of the more complex LSTM architecture.
[ 194 ]
Volatility Forecasting by LSTM
Chapter 5
Starting from the last configuration of our LSTM model, we can easily implement the simple RNN. First, we must import the SimpleRNN class from keras.layers using the following command: from keras.layers import SimpleRNN. After that, the only requirement is to write SimpleRNN instead of LSTM when we add the layers to the model. The results of this are summarized as follows: out[6]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 3.047241 1.290690 4.368046 0.397321 0.190000 0.210000 0.750000 1.070000 1.290000 1.330000 2.980000 1.470000 21.260000 2.190000
We can see that the performance of this simple RNN model is quite poor since the MSE statistics are slightly higher than those of the previous LSTM architecture.
The GARCH model We will also compare our LSTM with the GARCH model that can be seen as a benchmark for volatility forecasting. The GARCH model is an Autoregressive Moving Average (ARMA) model that can be applied to the variance of a time series dataset. GARCH is widely used in finance since the asset returns are volatile and often move together. The GARCH (p, q) model (Engle and Bollerslev, 1986) is given as follows:
The elements of this model are as follows: and are the number of lags. is the error term, which is assumed to be normally distributed with a mean of zero and conditional variance, . represents the returns of an asset at a time.
[ 195 ]
Volatility Forecasting by LSTM
Chapter 5
μ is the mean value of the returns and it is a positive value. ω, αi, and βi represent the model parameters. We can use the ARCH library in Python, which contains ARCH models and other tools for financial econometrics. This library is available in PyPI and can be installed with pip (pip install arch). You can find more information in the following link: https://arch. readthedocs.io/en/latest/index.html#. Follow these steps: 1. The following code applies the GARCH model to our initial dataset. This is done for each asset, as the model is univariate. First, we will import the needed libraries and define variables: # import required library from arch import arch_model # Rolling window forecasting window_length = 300
2. For each asset in our dataset, we load the related returns and initialize the GARCH model by calling the .arch_model() constructor. We invoke this method by passing as arguments the returns, the name of the model (vol='Garch'), and the number of lags, p and q, that we set to be 20 as the lookback in our LSTM model: mse = [] qlike = [] for i in range(len(assets)): # load returns ret = dataset[:, i*2] # Specify the model and estimating parameters am = arch_model(ret, vol='Garch', p=look_back, q=look_back)
3. We fit the ARCH model by using a rolling window with a fixed size of 300 days and then it is moved onestep from the last observation. We implement this rolling window using the first_obs and last_obs parameters of the .fit() method. After estimating the GARCH (20,20) model at each timestep, we make a prediction for the next day by calling .forecast() and setting horizon=1: forecasts = dict() forecasts['day'] = [] forecasts['fcast'] = [] for j in range(len(ret)  window_length):
[ 196 ]
Volatility Forecasting by LSTM
Chapter 5
# Estimate the model res = am.fit(first_obs=j, last_obs=j+window_length, disp='off') # Make forecast temp = res.forecast(horizon=1).variance fcast = temp.iloc[j + window_length  1] forecasts['day'].append(fcast.name) forecasts['fcast'].append(fcast.values) y_pred = np.array(forecasts['fcast']).reshape(1) y_true = dataset[window_length:, i*2+1]
4. Finally, we calculate the MSE and QLIKE metrics and save the results: # Calculate the evaluation metrics mse_i = (y_true  y_pred) ** 2 qlike_i = np.log(y_pred) + (y_true / y_pred) results = np.array([mse_i, qlike_i]).transpose() # save results np.savetxt('7GARCH/' + assets[i] + '.csv', results, delimiter=',', header='MSE, QLIKE',\ fmt='%10.5f', comments='') mse.append(np.mean(mse_i, axis=0)) qlike.append(np.mean(qlike_i, axis=0)) # save results results = np.array([mse, qlike]).transpose() np.savetxt('7.csv', results, delimiter=',', header='MSE, QLIKE', fmt='%10.5f',\ comments='')
The results of applying the GARCH (20,20) model are as follows: out[7]: count mean std min 25% 50% 75% max
MSE QLIKE 29.000000 29.000000 5.492759 1.463793 7.216270 0.381233 0.420000 0.350000 1.320000 1.270000 2.320000 1.480000 7.020000 1.710000 31.900000 2.290000
[ 197 ]
Volatility Forecasting by LSTM
Chapter 5
As can be seen from these results, we definitely obtain a worse performance here than when we used LSTM or even the simple RNN model.
Visualizing the cumulative squared error Finally, let's visually compare all of the methods used in this chapter. We calculate the cumulative squared error recorded by the different models by sorting the errors in decreasing order. The resulting curves show how each model tends to accumulate the errors along with the experimentation and provide a powerful visualization instrument for comparing them. The following code calculates the cumulative squared error for each model: # Folder where results are stored data_folder = ['1BASE', '2OL', '3OLLSTM2', '4OLLSTM2_softplus', '5OLLSTM2_softplus_QLIKE', '6RNN', '7GARCH' ] error_all = [] for folder in data_folder: error_f = np.empty((0, )) i = 0 for f in sorted(os.listdir(folder)): # Load errors error = pd.read_csv(folder + '/' + f, sep=',', usecols=[0, 1], engine='python') error = np.abs(error['MSE'].values) error_f = np.append(error_f, error.flatten(), axis=0) i += 1 # Calculate cumulative squared error error_f = np.sort(error_f)[::1] cum_error_f = np.cumsum(error_f) error_all.append(cum_error_f)
[ 198 ]
Volatility Forecasting by LSTM
Chapter 5
Let's now visualize the cumulative squared error calculated in the preceding calculation: # Label for plot labels = data_folder # Plot curves plt.figure() for i in range(len(error_all)): plt.plot(error_all[i], label=labels[i]) plt.legend() plt.show()
The following diagram shows the cumulative squared error curves for each forecasting model used in this chapter:
From the preceding plot, we can confirm that the LSTM with the final configuration (5) always performs better. Our first model (1) produced the worst results, even when compared to the GARCH model (7), which we use as a benchmark.
[ 199 ]
Volatility Forecasting by LSTM
Chapter 5
Summary In this chapter, we used an LSTM model to forecast the volatility of the daily returns of the stock prices belonging to the DJIA index together with the SPY, from January 2002 to August 2008. First, we saw how to appropriately prepare the dataset for the LSTM network. Then, we created, trained, and evaluated the model by using the Keras framework. Since results were not good enough, we improved the performance with different approaches, such as applying online training and stacking more LSTM layers. We also discussed how to choose the most suitable activation function of the output layer and loss function for our problem. Finally, we have made a comparison with simple RNN and GARCH models. Our results suggest that an LSTM can outperform the GARCH model that is a widely used, univariate, parametric benchmark. It also performs better than a simple RNN. This encourages us to use more complex and nonlinear models that can identify both short and longterm dependencies on data. In the next chapter, we will investigate how to identify buyandsell candidates by using technical indicators across S&P 500 stocks. The model will be based on CNN.
[ 200 ]
6 Trading Rule Identification by CNN A trade occurs when people agree on a price but disagree on the expectation of the value of the asset. This expectation is based on a forecast that could employ very complicated mathematics or that could be based on very simple rules. It is well established that human beings usually try to distill complex phenomena into simple rules that they can apply in similar situations. Technical analysis expands on this principle and assumes that market conditions repeat over time, thereby making rules applicable. You may have heard the phrase the trend is your friend. Trend analysis is one of the basic tenets of technical analysis and a large number of traders exploit these relationships. The justification as to why trend analysis still works is linked to the diffusion of information theory. Contrary to economic models, not all agents get information about assets instantaneously, which is one of the biggest drawbacks of the efficient market hypothesis. Information travels in bursts; some participants get the information later compared to other market participants. Hence, asset prices tend to trend until they don't! In this chapter, we will investigate a simple trading rule and then develop that trading rule using an unsupervised machine learning technique to improve it. First, we will discuss the trading rule that we want to investigate, then think about how to acquire the data to test this trading rule. After that, we will look at heuristic and machine learning constructs to improve upon the simple rule. We'll conclude this chapter by providing pointers regarding different aspects of this strategy and how it can be further researched so that you can get the most out of your learning. The following topics will be covered in this chapter: Trading signals with technical indicators Data handling Benchmarking Constructing a convolutional neural network
Trading Rule Identification by CNN
Chapter 6
Trading signals with technical indicators Financial time series data is noisy. Moving averages are a common method of smoothing time series data with the objective of removing the noise with minimal information loss. The objective of using moving averages is to decipher the underlying trends of prices. Consider the wellknown equation of asset prices:
When t is large, meaning that it is more than 1, the first term of the equation dominates. If we assume that the price distribution of the asset is constant, the last term of the preceding equation will be eliminated when computing returns using the preceding stochastic equation. The first term of the equation is the trend term, and this is where moving averages come in very handy. In practice, we use a multitude of moving averages as confirmatory signals of trends. One of the most common moving average combinations to be used is the 2050200day moving averages. The idea behind using this combination to predict a longterm trend is simple: The 20day moving average captures the shortterm trends in daily prices to represent information trades by market participants whose holding horizon is one month or less. The 50day moving average captures the mediumterm trends in daily prices to represent the strategies that capitalize on information related to quarterly data, such as earnings. The 200day moving average captures the longterm trends to represent the wellinformed investor who employs a buyandhold strategy and thus captures information that is strategic in nature. A combination of the three will result in a robust estimate of the first term in the equation. A typically simple heuristic measure is applied to get the forecast values of the trend terms. These are then ranked in terms of individual and composite strength to arrive at a trading strategy for shortterm profits. In this chapter, we will use the following simple trading rules: If the 20day moving average is higher than the 50day moving average, which, in turn, is higher than the 200day moving average, then the asset is a strong Buy for the 30day holding period.
[ 202 ]
Trading Rule Identification by CNN
Chapter 6
If the 20day moving average is lower than the 50day moving average, which, in turn, is lower than the 200day moving average, then the asset is a strong Sell for the 30day holding period. These simple rules aim to give you a strong idea of what to buy and what to sell that can then be further screened or used as is for trading decisions. We'll be using the aforementioned simple rules for trading decisions. The only data that's required for this strategy is the adjusted closing prices, which makes it a simple and powerful rule to use across a number of assets.
Data handling Data is the most critical building block of any learning model. We need to know of the quality and quantity of data to be able to train the model and have confidence in its predictive power. When using any learning model, the key assumption is that the data we are using comes from the same family of statistical distributions. Hence, we apply these models to specific asset classes because the rules governing asset prices are the same for an asset class and vary across asset classes. The following sections work on acquiring data and formatting it so that it can be used for prediction.
Getting data from public sources Daily price data is freely available for most liquid asset classes and can be downloaded from individual exchanges, financial data terminals such as Bloomberg, Reuters, or FactSet, or from sites such as Yahoo Finance or Google Finance. The Pandas package has functions that can be used to download daily price data from Yahoo Finance or Google Finance. For the purposes of this chapter, we will use Yahoo Finance as our data source for price data. After changing API, you won't be able to access it from the pandas package. However, you can scrape the historical data using the requests package and convert the data into JSON format, which can be read into a pandas DataFrame. Let's go over the code that can be used to download daily data for multiple symbols iteratively. We can define a class to create the url string by using the following: Symbol ID in Yahoo Finance format Start date and end dates
[ 203 ]
Trading Rule Identification by CNN
Chapter 6
This can be seen in the following code: class YahooDailyReader(): def __init__(self, symbol=None, start=None, end=None): import datetime, time self.symbol = symbol self.start = start self.end = end unix_start = int(time.mktime(self.start.timetuple())) day_end = self.end.replace(hour=23, minute=59, second=59) unix_end = int(time.mktime(day_end.timetuple())) url = 'https://finance.yahoo.com/quote/{}/history?' url += 'period1={}&period2={}' url += '&filter=history' url += '&interval=1d' url += '&frequency=1d' self.url = url.format(self.symbol, unix_start, unix_end)
Once the url string has been created, we can use the requests library to get historical data in a pandas DataFrame: def read(self): import requests, re, json import pandas as pd r = requests.get(self.url) ptrn = r'root\.App\.main = (.*?);\n}\(this\)\);' txt = re.search(ptrn, r.text, re.DOTALL).group(1) jsn = json.loads(txt) df = pd.DataFrame(jsn['context']['dispatcher']['stores'] ['HistoricalPriceStore']['prices']) df.insert(0, 'symbol', self.symbol) df['date'] = pd.to_datetime(df['date'], unit='s').dt.date df = df.dropna(subset=['close']) df = df[['date','symbol', 'open', 'high', 'low', 'close', 'adjclose', 'volume']] colnames = ['Date', 'symbolid', 'Open', 'High', 'Low', 'Close', 'AdjClose', 'Volume'] df.columns = colnames df = df.set_index('Date') return df
The resultant DataFrames, which contain historical price data, can be saved in flat file formats for later use. For the purposes of this chapter, we will use .csv files to store the daily data for further processing.
[ 204 ]
Trading Rule Identification by CNN
Chapter 6
Setting up the data We will download and use the daily price data for a large number of NASDAQ listed stocks. The accompanying repository contains a list of NASDAQ symbols for over 2,700 stocks and historical data going back as far as 1990. This gives us a dataset containing 8 million records that we can analyze. There are three stages in our strategy development: 1. Hypothesis formulation and testing with sample data 2. Hypothesis validation with outofsample data 3. Live testing to uncover any assumptions that might not hold true In the following subsections, we will discuss each of these stages briefly and how we can approach each aspect.
Hypothesis formulation and insample testing Keen observation and training is the basis of hypothesis formulation. A hypothesis might be formed as a result of a particular market behavior being observed or an opportunity being identified when using different techniques, models, or a combination of the two. In our case, we will use the hypothesis we mentioned previously—the 2050200day moving average hypothesis. The data required to test this hypothesis is simply the price data that we acquired using the preceding code. To test the hypothesis, we need to sample the data from our dataset. Sampling can be achieved in two ways: Time stratification: In this type of sampling, all assets are considered different and we can sample observations across time. Let's say that we use the first 10 years of daily data for testing purposes and the next 10 years of data for validation. We could also use a moving window concept. This would be a variant of time stratification, in which we would use the first 10 years of data for testing and the next two years of data for validation, and keep moving the testing and validation windows by a fixed period until the validation period ends at the last data point in our dataset. This technique should be used in cases where the hypothesis is dependent upon market regimes as a lot of information about the effectiveness of our hypothesis can be captured using this kind of analysis.
[ 205 ]
Trading Rule Identification by CNN
Chapter 6
Asset stratification: Asset prices are correlated. Not all assets are distinct and the comovement of different assets leads to feature reduction—whether using factor spaces or statistical features. This lends itself to the alternative approach of segregating across assets. Put simply, if you have price data for 1,000 stocks, the test sample might be the complete dataset for 700 stocks and the validation dataset might be the complete dataset for 300 stocks. When the hypothesis being tested is not deemed to be dependent upon market regimes, this is a good way of validating our theory. The pristine dataset for validation and model accuracy, which is similar to that of the test sample, reinforces our confidence in this approach. We will use the second method for our analysis in this chapter. As an exercise, conduct the same analysis using the first approach.
To test the efficacy of the model, we need forward returns for our actions. We can perform three possible actions: Buy the asset and hold it for 30 days and then sell it at the end of the period Do nothing Sell the asset short and cover the position after 30 days Due to this, we need to compute the long and short returns for a period of 30 days, which is our holding horizon. When computing the returns for buy and sell transactions, we assume that we have the worst possible intraday timing. This means we are able to buy at the highs of the day when buying into a new position or closing an old position and sell at the lows of the day when shorting or closing an old position. Taking this into account, we'll also consider a transaction cost of 50 bps per trade. This would more than compensate for the market impact and transaction costs for developed markets. The resultant tests would be robust to trader efficiency at micro scales. We'll also use the 5day, 10day, 30day, and 100day moving averages in addition to the 20day, 50day, and 200day moving averages. All these averages are scaled by the 200day moving average. This is for the reasons—To express all the values in terms of the 200day moving average, making comparisons easier. To reduce the feature space by filtering out the noise in the absolute prices:
[ 206 ]
Trading Rule Identification by CNN
Chapter 6
1. The following code prepares the sample data from the CSV files, which stores the daily data downloaded from Yahoo Finance. We're assuming a perfect foresight for optimal action: # create test and training data sets import pandas as pd import numpy as np import random
In the preceding code, we used the pandas and NumPy Python packages for data manipulation and storage. The pandas package is particularly useful since it has functions specifically suited for time series data. Next, we will set up the returns for both long positions and short positions. This will be useful for labeling the data. If the long returns are greater than 0, then we could label the decision to buy or sell. 2. The following function computes the returns to a long position. Here, the assumption is that the trader is extremely unskilled and would buy at the high of the day and sell at the low when the position is closed. The function has been parameterized so that the same can be used for different holding horizons: # decide upon the time period of interest for generating buy signals # Assume you can sell at the lows and buy at highs for that day only # Assume some transaction cost say 50 bps def long_returns(df, numdays): df['buyret'] = (df.Low / df.High.shift(numdays)1)*100 df.buyret.fillna(0, inplace=True) return df
3. The following function computes the returns to a short position. Here, the assumption is that the trader is extremely unskilled and would sell at the low of the day and buy at the high when the position is closed. The function has been parameterized so that the same can be used for different holding horizons: # decide upon the time period of interest for generating sell signals # Assume you can sell at the lows and buy at highs only # Assume some transaction cost say 50 bps def short_returns(df, numdays): df['sellret'] = (df.Low.shift(numdays) / df.High 1)*100 df.sellret.fillna(0,inplace=True) return df
[ 207 ]
Trading Rule Identification by CNN
Chapter 6
The data is labeled using the long and short returns computed using the functions preceding. 4. If the long return is greater than 0.5%, then we need to buy, while if the short return is greater than 0.5%, then we need to sell. The tolerance of 0.5% is to avoid making trades that would be lossmaking due to transaction costs. This may vary, depending on the trading efficiency of a fund house: # create labels def label_data(df): df['Action'] = 'None' df.loc[df['buyret'] > 0.5, 'Action'] = 'Buy' df.loc[df['sellret'] > 0.5, 'Action'] = 'Sell' # df = df[df.columns.drop(['buyret','sellret'])] return df
5. Now, let's write a generic function to compute the moving averages. Here, we rely upon the rolling method for a pandas DataFrame to compute the moving averages over rolling windows. This combined with the preceding buysell decision will give us a labeled DataFrame with indicators: flexible function for computing moving average values # normalise with variable that has the highest value def moving_avg_data(df, mavnames, mavdays): if(len(mavnames) != len(mavdays)): print('Variable Names and Number of days must match') return for i in range(0,len(mavnames)): df[mavnames[i]] = df.AdjClose.rolling(window = mavdays[i]).mean()
6. Now, scale all the moving averages by the maximum movingaverage day, as described in the hypothesis. This function is kept general to help in evaluating different criteria for scaling without the need to modify a lot of code: maxmovavg = mavnames[mavdays.index(max(mavdays))] mavnames.remove(maxmovavg) for i in range(0,len(mavnames)): df[mavnames[i]] = df[mavnames[i]] / df[maxmovavg] df.loc[:,maxmovavg] = 1 df.drop(df.index[:max(mavdays)],inplace=True) return df
[ 208 ]
Trading Rule Identification by CNN
Chapter 6
7. Create the datasets and save them in .csv files for iterative processing later on during the classification phase: def create_datasets(csvfilename, sample_size): test_num = random.sample(range(0,len(symlist)1), sample_size) data = pd.DataFrame() for i in range(0,len(test_num)): filename = 'csvdata/' + symlist.Symbol[test_num[i]] + '.csv' temp = pd.read_csv(filename) temp = temp[['Date', 'symbolid', 'Open', 'High', 'Low', 'Close', 'AdjClose', 'Volume']] mavnames = ['mav5', 'mav10','mav20','mav30','mav50','mav100','mav200'] mavdays = [5,10,20,30,50,100,200] fwdret = 30 temp = long_returns(temp, fwdret) temp = short_returns(temp, fwdret) temp = label_data(temp) temp = moving_avg_data(temp, mavnames, mavdays) temp = temp[['Date','symbolid','buyret','sellret','Action','mav5', 'mav10','mav20','mav30','mav50','mav100']] temp = temp.dropna() data = data.append(temp) data.to_csv('sampledata/'+csvfilename) print(csvfilename + ' written to disk')
The preceding function for creating datasets will be run for each of the data files by reading the symbols from descdata.csv and randomly selecting the desired number of symbols to create the dataset. This results in truly blind data that will be used for estimation, without the need to apply any prior classification, such as a sector/market cap or any other criteria. 8. Create some datasets with a few different iterations regarding the number of stocks, from 50 to 1,000. This will be helpful in the following situations: When evaluating the time it takes to fit models for different data sizes When training on one small set and testing on a larger set and vice versa to ascertain the robustness of the model
[ 209 ]
Trading Rule Identification by CNN
Chapter 6
Use the following code to do this: read the list of symbols file symlist = pd.read_csv('csvdata/descdata.csv') create_datasets('train_50.csv', 50) create_datasets('test_50.csv', 50) create_datasets('train_100.csv', 100) create_datasets('test_100.csv', 100) create_datasets('train_150.csv', 150) create_datasets('test_150.csv', 150) create_datasets('train_250.csv', 250) create_datasets('test_250.csv', 250) create_datasets('train_500.csv', 500) create_datasets('test_500.csv', 500) create_datasets('train_1000.csv', 1000) create_datasets('test_1000.csv', 1000)
Once created, these .csv files will be used for further analysis for testing simple rules and classification. However, for a convolutional neural network, further data processing is required, which will be discussed later. 9. When testing a hypothesis, in addition to accuracy, we are also interested in type 1 and type 2 errors. The intensity of misclassification is not symmetric. For example, if a Buy signal is incorrectly classified as Do nothing, the impact would be less severe than if it were classified as a Sell signal. The following diagnostic code would create this kind of matrix: import numpy as np import pandas as pd def prediction_accuracy(ytest, predict_val): # rows belong to prediction # columns to test values # order BUY, NONE , SELL accuracy_mat = np.zeros([3,3], dtype = float) for i in range(ytest.shape[1]): for j in range(predict_val.shape[1]): accuracy_mat[i,j] = sum(predict_val[(predict_val[:,j] * ytest[:,i] > 0),j]) allobs = sum(map(sum, accuracy_mat)) accuracy_mat = np.divide(accuracy_mat, allobs)*100 accuracy_mat = pd.DataFrame(accuracy_mat, columns = ['Buy', 'None', 'Sell'], index = ['Buy', 'None', 'Sell']) return accuracy_mat
[ 210 ]
Trading Rule Identification by CNN
Chapter 6
This accuracy matrix can be graphically displayed using the seaborn library. We will use the same datasets for different models for analysis throughout this chapter to compare the efficacy of each model while keeping the dataset at a control variable. The accuracy of the model is defined as all the observations correctly classified divided by the total number of observations. Regarding the accuracy matrix, the sum across the diagonal from the topleft corner to the bottomright corner is the measure of the accuracy of the model.
Benchmarking alternative models "Everything should be made as simple as possible but not simpler." – Albert Einstein The preceding quote captures the essence of mathematical modeling to solve problems. In our case, we will only apply more complicated models if there is an improvement in the prediction accuracy and explanatory power of the model. Otherwise, in general, a simple model is better than a complicated model. First, we will use a simple heuristic model that relies on the common notion of trend persistence. This will be followed by a more complicated classification model that's been adapted from the one we used to classify the iris dataset. Finally, we'll define our own deep convolutional network for feature extraction to discriminate between classes. Our objective is to identify the buy, sell, and hold signals from the price data correctly.
Benchmark 1 – simple trading rule A common heuristic in financial markets is trend persistence. The strength of a price trend is determined by the slope of the movingaverage line for the price series. Generally, moving averages over longer time horizons denote the strength of the trend, while moving averages over shorter time horizons depict temporary reversals.
[ 211 ]
Trading Rule Identification by CNN
Chapter 6
A simple trading rule is used to buy an asset when the shortterm moving averages are above the longterm moving averages, thereby cementing the strength of the trend. This is the opposite when you're selling an asset. Let's take a look: 1. First, we test the simple trading rule we described in the Trading signals with technical indicators section using the following code: import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline # in this trading rule if the 20 day moving average is over 50 day moving average is over 200 day moving average then buy in the reverse case sell # the data is already scaled for 200 day moving average def trading_rule_20_50_200(df): # initialize the new action column df['RuleAction'] = 'None'df.loc[((df['mav20'] > df['mav50']) & (df['mav50'] > 1)), 'RuleAction'] = 'Buy' df.loc[((df['mav20'] < df['mav50']) & (df['mav50'] < 1)), 'RuleAction'] = 'Sell' return df
2. Let's use the previously created training data file with 50 random stock data to test this rule: csvfilename = 'train_50.csv' data = pd.read_csv('sampledata/'+ csvfilename) data = trading_rule_20_50_200(data)
3. The data that's returned contains a column with the action taken based upon the rule, as defined in the function. The original data has the corresponding correct action, which we defined earlier: ytest = np.array(pd.get_dummies(data.Action)) predict_valid = np.array(pd.get_dummies(data.RuleAction)) df = prediction_accuracy(ytest, predict_valid)
[ 212 ]
Trading Rule Identification by CNN
Chapter 6
4. We can use the seaborn graphics package in Python to create a heatmap for correct and incorrect classification. This heatmap is our benchmark for more complicated models and we will continue to add complexity to the model if the heatmap improves: ax = sns.heatmap(df, annot=True, fmt="g", cmap='viridis') ax.xaxis.set_ticks_position('top') # now save the heatmap ax.figure.savefig('figures/trading_rule_50_50.png')
We apply the prediction accuracy function we defined in the Setting up the data section earlier to determine the accuracy matrix on one of the training datasets. The resultant accuracy matrix plot is as follows:
The accuracy of the simple model is about 40% for this small sample, which forms the starting point for us to understand how to create more complicated models. The low accuracy and high misclassification rates could be as a result of false signals, which is noise in the signal levels. This simple model does not distinguish between temporary shifts in moving averages. Most traders would do better than this.
[ 213 ]
Trading Rule Identification by CNN
Chapter 6
Benchmark 2 – simple classification network A quick literature survey of classification problems and algorithms highlights the use of Fisher's iris flower dataset, which is used for various classification algorithms, as the most cited benchmark for classification accuracy. We are interested in the use of a multilayer perceptron to classify the iris dataset. There are remarkable similarities between our problem and the iris dataset problem: Both use a few characteristics, such as petal length, color, and so on in the iris dataset, as well as moving averages. The flowers are classified into three categories, akin to our categories of buysell and hold. Due to this, we can adapt the network architecture that's used for Fisher's iris classification to our needs so that the features of action are the scaled movingaverage values. This network architecture can be found on Kaggle with the associated dataset. This dataset has been investigated thoroughly, and one of the papers that discusses this in detail can be found at https://www.researchgate.net/publication/268436755_An_Approach_for_ IRIS_Plant_Classification_Using_Neural_Network. The network architecture for classification problems is a simple multilayer perceptron. It has three input layers, one hidden layer, and one output layer. It is a fully connected perceptron with a sigmoid activation function. Let's get started: 1. Let's take a quick look at the distribution of our 50stock sample dataset using the following code: read the sample data file csvfilename = 'train_50.csv' data = pd.read_csv('sampledata/'+ csvfilename) # reshape dataframe data = data[['mav5', 'mav10', 'mav20', 'mav30', 'mav50', 'mav100', 'Action']] # plot to visualize the data g=sns.pairplot(data, hue="Action", height= 2.5) g.savefig('figures/train_50_desc.png')
The preceding code creates a diagnostics chart. We can use the seaborn package to generate a pairwise plot and save it as an image file:
[ 214 ]
Trading Rule Identification by CNN
Chapter 6
The preceding set of graphs shows how intertwined the data is with respect to the feature space. The high degree of correlation between the features is evidenced by the crosscorrelation. 2. This requires us to use a classification neural network, as follows: # simple classification using neural network # adapted from iris classification import os
[ 215 ]
Trading Rule Identification by CNN
Chapter 6
import tensorflow as tf import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from IPython.display import display, Image from pandas import get_dummies from sklearn.cross_validation import train_test_split # Config the matlplotlib backend as plotting inline in IPython %matplotlib inline
Let's go over what we did in the preceding code: We used the TensorFlow library to define and train our perceptron. Chapter 2, Designing Neural Network Architectures, and Chapter 3, Constructing, Testing, and Validating Models, contain more details about the TensorFlow library. The sklearn package is used to split the training and test datasets for training and validating the network and then testing the network. The matplotlib package will be used to display the results of training and validation, while a heatmap regarding prediction accuracy will be generated using seaborn graphics. We could use TensorBoard instead of Matplotlib to get an interactive feel of the training and validation process.
3. Now, let's set up the data structures that will be used to train the network architecture that we'll define later using TensorFlow objects. Our classification labels are text but need to be converted into a numeric format for training. The get_dummies function does this conversion for us: cols = data.columns features = cols[0:6] labels = cols[6] print(features) print(labels) X = data[features] y = data[labels] y = get_dummies(y)
[ 216 ]
Trading Rule Identification by CNN
Chapter 6
4. The training dataset contains two matrices: one for features and one for labels. The features matrix is defined as X while the labels matrix is defined as Y, which keeps in tune with the conventions for a standard regression model: X_train = np.array(X).astype(np.float32) y_train = np.array(y).astype(np.float32)
5. The preceding steps need to be followed for the test dataset as well. We're doing this to test the outofsample accuracy of our model: csvfilename = 'test_50.csv' data = pd.read_csv('sampledata/'+ csvfilename) # reshape dataframe data = data[['mav5', 'mav10', 'mav20', 'mav30', 'mav50', 'mav100', 'Action']] g=sns.pairplot(data, hue="Action", height= 2.5) g.savefig('figures/test_50_desc.png') X = data[features] y = data[labels] y = get_dummies(y) X_test = np.array(X).astype(np.float32) y_test = np.array(y).astype(np.float32) print(X_train.shape, y_train.shape) print(X_test.shape, y_test.shape)
6. Ascertain the training and test shapes so that they're in the required TensorFlow format and define the number of features, labels, and hidden layers. The numberofhiddenlayers variable will be used later when we define a deep network. In the current perceptron, there is only one fully connected hidden layer: training_size = X_train.shape[1] test_size = X_test.shape[1] num_features = 6 num_labels = 3 num_hidden = 10
7. With the preceding parameters defined, we are ready to construct our network using TensorFlow: graph = tf.Graph() with graph.as_default(): tf_train_set = tf.constant(X_train) tf_train_labels = tf.constant(y_train) tf_valid_set = tf.constant(X_test) print(tf_train_set) print(tf_train_labels) weights_1 = tf.Variable(tf.truncated_normal([num_features,
[ 217 ]
Trading Rule Identification by CNN
Chapter 6
num_hidden])) weights_2 = tf.Variable(tf.truncated_normal([num_hidden, num_labels])) bias_1 = tf.Variable(tf.zeros([num_hidden])) bias_2 = tf.Variable(tf.zeros([num_labels])) logits_1 = tf.matmul(tf_train_set , weights_1 ) + bias_1 rel_1 = tf.nn.relu(logits_1) logits_2 = tf.matmul(rel_1, weights_2) + bias_2 soft = tf.nn.softmax(logits_2) loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logit s_2, labels=tf_train_labels)) optimizer = tf.train.GradientDescentOptimizer(.005).minimize(loss)
The preceding code defines a multilayer perceptron with no hidden layers and softmax to output the class probability. The optimization method that's used is gradient descent and can be stuck in the local optima. 8. The following code predicts using the training and validation datasets in order to estimate the weights of our perceptron. It then uses those to validate and prevent overfitting: ## Training prediction predict_train = tf.nn.softmax(logits_2) # Validation prediction logits_1_val = tf.matmul(tf_valid_set, weights_1) + bias_1 rel_1_val = tf.nn.relu(logits_1_val) logits_2_val = tf.matmul(rel_1_val, weights_2) + bias_2 predict_valid = tf.nn.softmax(logits_2_val)
9. Define the fitness criterion as the percentage of predictions that agree with the labels: def accuracy(predictions, labels): return (100.0 * np.sum(np.argmax(predictions, 1) == np.argmax(labels, 1)) / predictions.shape[0])
10. Save the graph and create a directory to save the checkpoints for future analysis: with graph.as_default(): saver = tf.train.Saver() if (os.path.exists('simpleclass') == False): !mkdir bschkpntcnn
[ 218 ]
Trading Rule Identification by CNN
Chapter 6
11. Now, we are ready to train our perceptron. We can train for, say, 10,000 iterations and check if there is an increase in the classification accuracy. Since this is a simple network and each iteration doesn't take a long time to complete, we can train for 10,000 steps and evaluate if there is a plateau in training accuracy: num_steps = 10000 with tf.Session(graph = graph) as session: session.run(tf.global_variables_initializer()) print(loss.eval()) for step in range(num_steps): _,l, predictions = session.run([optimizer, loss, predict_train]) if (step % 2000 == 0): #print(predictions[3:6]) print('Loss at step %d: %f' % (step, l)) print('Training accuracy: %.1f%%' % accuracy( predictions, y_train[:, :])) print('Validation accuracy: %.1f%%' % accuracy(predict_valid.eval(), y_test)) predict_valid_arr = predict_valid.eval() saver.save(session,"simpleclass/simple.ckpt")
12. The results from the preceding training can be saved in HDF file format for further analysis: save the results import h5py hf = h5py.File('h5files/simpleclass_train_50.h5', 'w') hf.create_dataset('predict_valid', data=predict_valid_arr) hf.create_dataset('y_test', data = y_test) hf.close()
13. The saved training results can be used to plot the heatmap for the accuracy of results. This is done so that multiple models can be trained with varying sample sizes and plotted in one go so that we can check if the network accuracy deteriorates if there is a difference in the training sample and test sample sizes: hf = h5py.File('h5files/simpleclass_train_50.h5', 'r') predict_val = hf['predict_valid'][:] ytest = hf['y_test'][:] x = np.argmax(predict_val, axis = 1) predict_valid = np.zeros(predict_val.shape) predict_valid[x == 0,0] = 1 predict_valid[x == 1,1] = 1 predict_valid[x == 2,2] = 1 df = prediction_accuracy(ytest, predict_valid)
[ 219 ]
Trading Rule Identification by CNN
Chapter 6
ax = sns.heatmap(df, annot=True, fmt="g", cmap='viridis') ax.xaxis.set_ticks_position('top') ax.figure.savefig('figures/simpleclass_50_50.png') plt.clf()
The resultant accuracy matrix for the classification of the validation dataset is as follows:
This more intelligent classification method shows a remarkable increase in the prediction accuracy from 40% to 64% while keeping the severe misclassification probabilities (the Buy as Sell and Sell as Buy cells) the same as they are in the naive model. Now, this becomes the new benchmark!
Constructing a convolutional neural network In Chapter 2, Designing Neural Network Architectures, we discussed convolutional neural networks in detail. You are advised to read that chapter before working on the example provided here.
[ 220 ]
Trading Rule Identification by CNN
Chapter 6
A typical human decision process to filter noise is used to observe a few data points and absorb the information as a picture. It is very unlikely that a trader would take a decision after observing the first data point. When a trader sees the price changes going up every day, their belief in wanting to trade increases. A typical process is to observe the past patterns and then match new data to old patterns and perform an action that is consistent with the patterns if they match with significant confidence. Convolutional neural networks can be applied in this scenario to automate the human decisionmaking process and apply it across multiple assets.
Modeling investment logic Given that our holding horizon is 30 days, it would be best to observe the data for about a third of this time before making a trade. Let's say that we observe the movingaverage data over 12 days and find the corresponding action set. The action we are likely to take is the one that occurs the most frequently. Let's say that there are multiple crossovers and that the movement is confusing. In this case, it would be best to do nothing. However, if 8 out of 12 times the action results in a buy, we would likely choose to buy. The idea is to identify patterns in the data that would confirm the action and then take the appropriate action (buy/sell/do nothing) for validation.
Selecting the network architecture Selecting the appropriate architecture depends on the input data. Since there are temporal features in data that can only be deciphered if the input is an array that's used to capture the persistence and relative position of each of the data points, we define the input as a tensor. Hence, we can use multiple data points of each of the features to capture the volatility in this data. Selecting a convolutional layer is also dependent on how closely we would like to analyze this data. Since we are only interested in capturing the essence that if the moving averages over lower time horizons are either above or below the moving averages of a higher time horizon, we can use bigger convolutional windows. Architecture selection is an art rather than a science, so the aforementioned heuristics can be used as guides rather than rules. The CNN architecture that we would use for our problem would be as follows: An input layer: This is a 12 x 3 tensor, or 12 observations of three features. A convolutional layer: We convolve to a space of a 6 x 6 tensor to capture the pattern.
[ 221 ]
Trading Rule Identification by CNN
Chapter 6
A pooling layer to flatten. An output layer of one of three classes.
Setting up the data in the correct format When segmenting the data, we follow the process of using 12 observations of data. However, as in human cognition, the windows of observation are overlapping, so we don't throw away information that would arise when moving a few data points. For simplicity, the window size is half of the segment size. Experiment with different window sizes to determine the optimal size.
The following code sets up the datasets into NumPy arrays that can be fed to a TensorFlow CNN. These are then saved in h5 files so that they can be recalled quickly during training and testing with different parameters. A few utility functions are included, which will be used in the main TensorFlow code. Let's get started: 1. Import the required libraries: import pandas as pd import numpy as np from scipy import stats import tensorflow as tf from sklearn.model_selection import train_test_split import h5py
The preceding code imports the necessary libraries, the most important of which are scikitlearn and tensorflow. The former splits the dataset into training and testing, while the latter constructs our deep network. The other libraries are used for data processing. 2. Split the data into windows of the required time horizon: def windows(data, size): start = 0 while start < data.count(): yield int(start), int(start + size) start += (size / 2)
[ 222 ]
Trading Rule Identification by CNN
Chapter 6
The preceding code starts with the first data point, which constructs a dataset over the defined window, then moves over by half the window size to create the next dataset. This means the windows are overlapping. This is akin to longterm information being lost but shortterm information being retained and influencing decisionmaking. 3. Segment the data into a signal and its corresponding label: def segment_signal(data,window_size = 12): segments = np.empty((0,window_size,6)) labels = np.empty((0)) for (start, end) in windows(data['Date'], window_size): x = data["mav5"][start:end] y = data["mav10"][start:end] z = data["mav20"][start:end] a = data["mav30"][start:end] b = data["mav50"][start:end] c = data["mav100"][start:end] if(len(data['Date'][start:end]) == window_size): segments = np.vstack([segments,np.dstack([x,y,z,a,b,c])]) labels = np.append(labels,stats.mode(data["Action"][start:end])[0][0]) return segments, labels
This creates NumPy arrays of data segments and corresponding labels. 4. Create batches from these segments that can be used to train our model: def get_batches(X, y, batch_size = 100): """ Return a generator for batches """ n_batches = len(X) // batch_size X, y = X[:n_batches*batch_size], y[:n_batches*batch_size] # Loop over batches and yield for b in range(0, len(X), batch_size): yield X[b:b+batch_size], y[b:b+batch_size]
The network is trained in batches to avoid memory overflow. 5. Create the training and test data in a format that can be used by TensorFlow by following the preceding steps: def create_tensorflow_train_data(csvfilename): df = pd.read_csv('sampledata/'+ csvfilename) df = df[['Date','symbolid','buyret','sellret','Action','mav5', 'mav10','mav20','mav30','mav50','mav100']] symbols = df.symbolid.unique() segments, labels = segment_signal(df[df.symbolid == symbols[0]])
[ 223 ]
Trading Rule Identification by CNN
Chapter 6
df = df[df.symbolid != symbols[0]] symbols = symbols[1:] for i in range(0,len(symbols)): x, a = segment_signal(df[df.symbolid == symbols[i]]) segments = np.concatenate((segments, x), axis = 0) labels = np.concatenate((labels, a), axis = 0) df = df[df.symbolid != symbols[i]] print(str(round(i/len(symbols)*100,2)) + ' percent done') list_ch_train = pd.get_dummies(labels) list_ch_train = np.asarray(list_ch_train.columns) labels = np.asarray(pd.get_dummies(labels), dtype = np.int8) X_tr, X_vld, lab_tr, lab_vld = train_test_split(segments, labels, stratify = labels, random_state = 123) return X_tr, X_vld, lab_tr, lab_vld, list_ch_train
The preceding code uses data from the test dataset, which we stored in CSV files in the Setting up data section, and uses the functions we defined previously to create NumPy arrays that will be used by TensorFlow when training the network. The same is done for the test data as set as well, as shown in the following code: def create_tensorflow_test_data(csvfilename): df = pd.read_csv('sampledata/'+ csvfilename) df = df[['Date','symbolid','buyret','sellret','Action','mav5', 'mav10','mav20','mav30','mav50','mav100']] list_ch_test = df.Action.unique() symbols = df.symbolid.unique() segments, labels = segment_signal(df[df.symbolid == symbols[0]]) df = df[df.symbolid != symbols[0]] symbols = symbols[1:] for i in range(0,len(symbols)): x, a = segment_signal(df[df.symbolid == symbols[i]]) segments = np.concatenate((segments, x), axis = 0) labels = np.concatenate((labels, a), axis = 0) df = df[df.symbolid != symbols[i]] print(str(round(i/len(symbols)*100,2)) + ' percent done') list_ch_test = pd.get_dummies(labels) list_ch_test = np.asarray(list_ch_train.columns) labels = np.asarray(pd.get_dummies(labels), dtype = np.int8) X_test = segments y_test = labels return X_test, y_test, list_ch_test
[ 224 ]
Trading Rule Identification by CNN
Chapter 6
6. The training and test data we created using the preceding functions can be saved as h5 files that can be read when the network needs to be trained again. The following utility functions read the TensorFlow formatted data stored in the h5 files: def get_tf_train_data(h5filename): hf = h5py.File('h5files/' + h5filename, 'r') X_tr = hf['X_tr'][:] X_vld = hf['X_vld'][:] lab_tr = hf['lab_tr'][:] lab_vld = hf['lab_vld'][:] list_ch_train = hf['list_ch_train'][:] hf.close() return X_tr, X_vld, lab_tr, lab_vld, list_ch_train def get_tf_test_data(h5filename): hf = h5py.File('h5files/' + h5filename, 'r') X_test = hf['X_test'][:] y_test = hf['y_test'][:] list_ch_test = hf['list_ch_test'][:] return X_test, y_test, list_ch_test
The preceding code can be stored in a file called tensorflow_setup.py so that it can be used later when we execute the network.
Training and testing the model The previous section detailed setting up data that can be used to train our convolutional network. This is in line with the inputs that are expected by the network, as defined in the Benchmark 2—Simple Classification Network section. Now, let's start training and testing the model: 1. The following code is used to set up the variables that will be used when we set up the network and test it for the datasets saved earlier in the h5 file format. We save the training accuracy and loss as figures that can be reviewed once training has been completed. Similarly, the training and validation results are saved as a HDF file that can be analyzed later: from tensorflow_setup import * import tensorflow as tf import os import matplotlib.pyplot as plt #%matplotlib inline # filenames used
[ 225 ]
Trading Rule Identification by CNN
Chapter 6
traindtfile = 'hdf_50.h5' testdtfile = 'hdf_50.h5' losssavefig = 'cnn_train_50_loss.png' accsavefig = 'cnn_train_50_accuracy.png' resultsave = 'cnn_train_50.h5' chkpointdir = 'cnn50/' X_tr, X_vld, y_tr, y_vld, list_ch_train = get_tf_train_data('csvfile/'+traindtfile)
This network is more complex than the simple perceptron, so training will be done in batches and the epochs will be limited to 1,000 rather than 10,000. Using batches is a great way to prevent the data from overfitting. 2. All the hyperparameters can be tuned even further. Let's use a learning rate of 0.0001 as a starting point: batch_size = 600 # Batch size seq_len = 12 # Number of steps learning_rate = 0.0001 epochs = 1000
3. Next, we need to predict three action outcomes: either to buy, sell, or do nothing. It is assumed that if the signal is buy, then it is for the time horizon that the return has been computed for. This network has six input nodes, which are the moving averages, so there are six channels. The preceding hyperparameters and the following definitions are used to set up a TensorFlow graph: n_classes = 3 # buy sell and nothing n_channels = 6 # moving averages graph = tf.Graph()
4. With the graph initialized, we need some placeholders for the input data, labels, and so on. These will be fed with the training and validation data: # Construct placeholders with graph.as_default(): inputs_ = tf.placeholder(tf.float32, [None, seq_len, n_channels], name = 'inputs') labels_ = tf.placeholder(tf.float32, [None, n_classes], name = 'labels') keep_prob_ = tf.placeholder(tf.float32, name = 'keep') learning_rate_ = tf.placeholder(tf.float32, name = 'learning_rate')
[ 226 ]
Trading Rule Identification by CNN
Chapter 6
5. The convolutional layer is the first layer of a convolutional neural network. This is used to create a feature map that distills the salient characteristics of the input data. The feature map, as a result, is sensitive to the position. Hence, we need to perform pooling in order to downsample the presence of features in the feature map space. The dropout function during the training phase of the network prevents overfitting data: with graph.as_default(): # (batch, 12, 3) > (batch, 6, 6) conv1 = tf.layers.conv1d(inputs=inputs_, filters=6, kernel_size=2, strides=1, padding='same', activation = tf.nn.relu) max_pool_1 = tf.layers.max_pooling1d(inputs=conv1, pool_size=2, strides=2, padding='same') with graph.as_default(): # Flatten and add dropout flat = tf.reshape(max_pool_1, (1, 6*6)) flat = tf.nn.dropout(flat, keep_prob=keep_prob_)
6. Softmax is employed to predict the likelihood of the outcome belonging to one of the three classes (buy, sell, and none). The loss function is estimated by crossentropy, which compares each predicted output to the actual class output. The score is then computed, which is penalized based on its distance from the actual output. For classification problems, class accuracy is critical: # Predictions logits = tf.layers.dense(flat, n_classes) soft = tf.argmax(logits,1) pred = tf.nn.softmax(logits,1) # Cost function and optimizer cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logit s, labels=labels_)) optimizer = tf.train.AdamOptimizer(learning_rate_).minimize(cost) # Accuracy correct_pred = tf.equal(tf.argmax(logits, 1), tf.argmax(labels_, 1)) accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name='accuracy')
Checkpoints are a great way of saving results so that we can retrain a network from a previously stored state.
[ 227 ]
Trading Rule Identification by CNN
Chapter 6
7. The following code creates a directory where checkpoints can be saved. At each checkpoint, the efficacy of the network is saved: if (os.path.exists(chkpointdir) == False): !mkdir chkpointdir validation_acc = [] validation_loss = [] train_acc = [] train_loss = [] with graph.as_default(): saver = tf.train.Saver()
Now that the TensorFlow model has been defined and saved in the graph, we can train the network and periodically view the results. These depict the improvement in training accuracy. If the training accuracy plateaus, the network has been trained successfully and it can be used to make predictions on test data. Checkpoints are helpful since any further training can be done from a checkpoint rather than initial stages. 8. Now, we can run our training on the convolutional network we defined earlier. The CNN is saved as a graph and training is conducted in a session: with tf.Session(graph=graph) as sess: sess.run(tf.global_variables_initializer()) iteration = 1 # Loop over epochs for e in range(epochs): # Loop over batches for x,y in get_batches(X_tr, y_tr, batch_size): #Feed dictionary feed = {inputs_ : x, labels_ : y, keep_prob_ : 0.5, learning_rate_ : learning_rate} # Loss loss, _ , acc = sess.run([cost, optimizer, accuracy], feed_dict = feed) train_acc.append(acc) train_loss.append(loss)
[ 228 ]
Trading Rule Identification by CNN
Chapter 6
9. For every few iterations, print the results so that the user can monitor the progress of the network. If no improvement can be seen, it can be stopped and evaluated on the test data: # Print at each 5 iters if (iteration % 5 == 0): print("Epoch: {}/{}".format(e, epochs), "Iteration: {:d}".format(iteration), "Train loss: {:6f}".format(loss), "Train acc: {:.6f}".format(acc))
10. After every 10 iterations, we can use the validation data to test the outofsample data. This is done to check if the training is stuck in a local optimum which may result in overfitting. The characteristics of the validation data that predict the accuracy of the network are printed for every 10 iterations. This data is stored at each checkpoint: Compute validation loss at every 10 iterations if (iteration%10 == 0): val_acc_ = [] val_loss_ = [] for x_v, y_v in get_batches(X_vld, y_vld, batch_size): # Feed feed = {inputs_ : x_v, labels_ : y_v, keep_prob_ : 1.0} # Loss loss_v, acc_v = sess.run([cost, accuracy], feed_dict = feed) val_acc_.append(acc_v) val_loss_.append(loss_v) # Print info print("Epoch: {}/{}".format(e, epochs), "Iteration: {:d}".format(iteration), "Validation loss: {:6f}".format(np.mean(val_loss_)), "Validation acc: {:.6f}".format(np.mean(val_acc_))) # Store validation_acc.append(np.mean(val_acc_)) validation_loss.append(np.mean(val_loss_)) # Iterate iteration += 1 saver.save(sess,chkpointdir + "bs.ckpt")
[ 229 ]
Trading Rule Identification by CNN
Chapter 6
11. Now that we have the training and validation results, we can plot the trajectory of the training accuracy and validation accuracy to check at which iteration the learning process plateaued. We use the matplotlib library for these plots and save the figures: t = np.arange(iteration1) plt.figure(figsize = (6,6)) plt.plot(t, np.array(train_loss), 'r', t[t % 10 == 0], np.array(validation_loss), 'b*') plt.xlabel("iteration") plt.ylabel("Loss") plt.legend(['train', 'validation'], loc='upper right') plt.savefig('figures/'+losssavefig) # Plot Accuracies plt.figure(figsize = (6,6)) plt.plot(t, np.array(train_acc), 'r', t[t % 10 == 0], validation_acc, 'b*') plt.xlabel("iteration") plt.ylabel("Accuracy") plt.legend(['train', 'validation'], loc='upper right') plt.savefig('figures/'+accsavefig) del(X_tr, X_vld, y_tr, y_vld, list_ch_train)
Once we are happy with our network and the hyperparameters have been tuned, it is time to test the network with the test dataset. The test dataset is constructed from randomly selected stocks. We can use any of the test datasets to test the network. The process of data preparation for the test data is exactly the same as it is for training and validation data. Also, the process of testing the network is the same as that for validation data. 12. Now, format the test data and see how well the network classifies the outofsample data. The network predicts the probability of a class rather than certainty: X_test, y_test, lab_ch_test = get_tf_test_data(testdtfile) test_acc = [] probs = []
[ 230 ]
Trading Rule Identification by CNN
Chapter 6
13. We create a new TensorFlow session and restore the network from the last saved checkpoint, which is the checkpoint when the network was fully trained. This means we're getting the trained network graph in this session: with tf.Session(graph=graph) as sess: # Restore saver.restore(sess, tf.train.latest_checkpoint(chkpointdir)) for x_t, y_t in get_batches(X_test, y_test, batch_size): feed = {inputs_: x_t, labels_: y_t, keep_prob_: 1}
14. Now, feed the test data and save the predictions into the feed dictionary. As with the validation dataset, we can save the predictions: batch_acc = sess.run(accuracy, feed_dict=feed) test_acc.append(batch_acc) prob = sess.run(pred, feed_dict=feed) probs.append(prob) print("Test accuracy: {:.6f}".format(np.mean(test_acc))) # now reshape the probs array probs = np.array(probs) probs = probs.reshape((probs.shape[0]*probs.shape[1]), probs.shape[2]) y_test = y_test[:len(probs),:] # model complete # save results import h5py hf = h5py.File('h5files/' + resultsave, 'w') hf.create_dataset('predict_valid', data=probs) hf.create_dataset('y_test', data = y_test) hf.close() del(X_test, y_test, lab_ch_test)
We keep a record of predicted probabilities rather than predictions since it is easy to deduce the latter from the former. Analyzing this information helps us understand whether we need to improve the network architecture by using some other activation functions or making the model more complex by adding more layers.
[ 231 ]
Trading Rule Identification by CNN
Chapter 6
The diagnostics for the preceding dataset, when using the CNN architecture, can be seen in the following graph:
As we can see, the accuracy of the model is greatly improved by 78%. The sum of the probabilities is across the diagonal. The misclassification errors have been reduced to less than 1%. Thus, this model is a good starting point for developing a productionready model with constraints that would form a part of portfolio construction and trading strategy.
Summary This chapter demonstrated the power of a convolutional neural network for classification problems over other classification algorithms when the data is a feature and as noisy as the financial price data. We learned how to use the TensorFlow library to define a multilayer perceptron and a convolutional neural network, and then trained these networks with real data. We also delved deep into the characteristics of return data using moving averages and compared different models using the same accuracy matrix construct. The result was an effective trading signal.
[ 232 ]
Trading Rule Identification by CNN
Chapter 6
As we've seen, CNNs can be used with simple data to derive effective trading rules. This can be exploited in a number of ways. CNNs can be used for strategy development. They can then be enhanced using portfolio construction and trading tools. We could also use these CNN systems to complement the work of expert traders, since it would, in principle, resemble their thought process. This would allow traders to tweak their model or increase their set of assets for better trading P&L. This could also be done as part of asset allocation or the alpha model strategy and used in lieu of a trading model when combined with the alpha signal. In the next chapter, we'll augment a convolutional network with an LSTM network to form a hybrid structure. This will allow us to forecast country indices for asset allocation.
[ 233 ]
3 Section 3: Hybrid Models In this section, you will determine how to combine the building blocks presented in Section 2, Foundational Architectures, in a more complex architecture, where each block plays a specific functional role. This section comprises the following chapters: Chapter 7, Asset Allocation by Long ShortTerm Memory over CNN Chapter 8, Digesting News by NLP with BLSTM Chapter 9, Risk Measurement Using GAN
7 Asset Allocation by LSTM over a CNN Asset allocation is the most important aspect of an asset management firm and is generally handled by the office of the Chief Investment Officer. This person could be a generalist, or there could be a team of specialists assisting a generalist to decide upon allocating capital across asset classes. The definition of an asset class can be as wide as bonds, equities, or commodities, or as narrow as a sector within a country for bonds or equities, precious metals as a commodity group, or even a custom group of securities. In this chapter, we will implement and test the efficiency of a hybrid Convolutional Neural Network (CNN)/Long ShortTerm Memory (LSTM) model for asset allocation problem. The CNN model is used to detect features in underlying crosssectional data, while the LSTM model is used to measure the persistence of these features. You will understand the advantage of using this model versus statistical frameworks such as Bayesian Vector Autoregression (BVAR). The following topics will be covered in the chapter: Modeling tactical asset allocation Understanding the deep learning model Implementing a CNNLSTM model Testing and validating our model
Asset Allocation by LSTM over a CNN
Chapter 7
Modeling tactical asset allocation There are two kinds of asset allocation decisions: strategic asset allocation (SAA) decisions, where a firm decides upon a neutral benchmark in line with its objectives, and tactical asset allocation (TAA) decisions, which are undertaken to capture temporary mispricings in asset classes versus the neutral benchmark. SAA is an infrequent exercise, undertaken once every 5 or 10 years, taking into account both quantitative and qualitative factors, and is not the topic of interest for this chapter. We will instead focus on TAA, which is more interesting at a daytoday level and lends itself more to rely on data for shortterm forecasting. Given the increase in exchangetraded fund (ETF) markets and moves towards index funds, interest in TAA has grown exponentially over the years. A large body of academic literature has confirmed that asset allocation policy is the dominant factor in portfolio performance. When asset allocation decisions are tactically managed, the performance of the portfolio will be largely determined by the performance of TAA. One of the few seminal studies in putting asset allocation decisions into a general coherent framework is due to Sharpe (Integrated Asset Allocation, Financial Analysts Journal, 1987). Implicitly based on a oneperiod model, Sharpe argues that, given the same investment opportunity set, investors with different attitudes toward risk will have different responses to asset allocation decisions. An investor will stay with a constantmix strategic portfolio, provided that relative risk tolerance does not change, and expected returns, risks, and correlations are all believed to be constant. However, when the investor attempts to predict expected returns and covariance matrix, or when these parameters are believed to be timevarying, the investor will choose TAA. The first step in implementing a TAA model is to define the assets that are to be modeled for expected returns, the time horizon for these returns, and an objective function we aim to optimize.
Defining our problem Consider the problem for an equities TAA manager of allocating to stock market indices across different countries. In this case, the manager needs to forecast returns for each of the country indices and allocate capital across these indices to maximize investment performance.
[ 236 ]
Asset Allocation by LSTM over a CNN
Chapter 7
The following datasets are most commonly used for such problems: 1. 2. 3. 4.
Index Level (IL) data, to compute forward returns for country indices Price to Earnings (PE) ratio, which is a measure of risk premia Price to Book (PB) ratio, which is a measure of asset leverage Price to Sales (PS) ratio, which is a measure of asset efficiency
While the economic logic is sound in using the preceding ratios individually or in some combination—for example, the Brinson Model uses ROE = PE / PB (ROE being Return on Equity) in making a tactical bet—we refrain from using this to guide our model definition. There are two approaches to tackle the problem.
Joint forecasting for an asset class The classical approach to asset allocation assumes the following: The expected alpha for each asset class follows the same distribution. The distribution of the error structure is well defined and invariant. The joint distribution of the return structure is independent across time. While each of the preceding assumptions can be argued and rejected, thus lending little credence to the theory, we will focus on the practical aspects of our problem. A key problem with the classical approach is that, when we minimize errors in forecasting, we reduce the emphasis on good forecasts. Thus, error averaging results in reducing the forecasting confidence of some forecasts and increasing forecasting confidence of others, without clear distinction. Hence, the bets work on average!!
Individual forecasting and bets Returns to a country's stock market index are driven by the following: 1. Domestic capital: This is the capital that can only be allocated to the said market due to regulatory, compliance, or consumption considerations. The investors in this category typically allocate between this asset class and other asset classes within the same geographical area. There will always be some allocation depending upon expectations of local investors and homecountry bias. 2. International capital: This capital, funded from investors who do not belong to the geographical area of the market, is purely seeking returns relative to other markets and allocating across different asset classes to maximize overall returns.
[ 237 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Both of the preceding considerations result in capital being allocated to an asset class if the return expectation of that asset class is attractive compared to return expectations of other asset classes. We will focus our efforts on making individual forecasts. Hence, we create one model per country and use the results of the model to understand how powerful that model is before using it to make bets. Investment logic is simple. Bet on forecasts where models are more accurate while keeping a neutral position for countries where forecasts are unreliable.
Setting up data We will use the countrylevel data for developed markets as defined by Morgan Stanley Capital International (MSCI). This historical daily dataset is provided in three CSV formatted files and can be read into pandas DataFrames for further processing, as shown in the following code block: import pandas as pd import numpy as np from matplotlib import pyplot # read formatted data from csv files # Developed markets data will be used to train and validate the model dm_index = pd.read_csv('dm_index.csv',sep=',',index_col=0) dm_pe = pd.read_csv('dm_pe.csv',sep=',',index_col=0) dm_pb = pd.read_csv('dm_pb.csv',sep=',',index_col=0) dm_ps = pd.read_csv('dm_ps.csv',sep=',',index_col=0) # compute one month forward returns dm_returns = dm_index.shift(22)/dm_index 1 # drop all values which are na dm_returns = dm_returns.dropna()
Allocation is done based upon the relative attractiveness of a country's index. The factor of interest for this attractiveness is the one month forward return for a country's index relative to indices of other countries. However, the predictor variables also have little meaning in terms of absolute values but need to be considered relative to each other. Hence, all the values need to be scaled from 0 to 1, as a measure of relative attractiveness, as shown here: now scale all of the above scaled_dm_returns = dm_returns.apply(lambda x:(xx.min()) / (x.max()x.min()),axis=1)
[ 238 ]
Asset Allocation by LSTM over a CNN
Chapter 7
scaled_dm_pe = dm_pe.apply(lambda x:(xx.min()) / (x.max()x.min()),axis=1) scaled_dm_pb = dm_pb.apply(lambda x:(xx.min()) / (x.max()x.min()),axis=1) scaled_dm_ps = dm_ps.apply(lambda x:(xx.min()) / (x.max()x.min()),axis=1) # align the dataset scaled_dm_pe = scaled_dm_pe[scaled_dm_pe.index.isin(scaled_dm_returns.index)] scaled_dm_pb = scaled_dm_pb[scaled_dm_pb.index.isin(scaled_dm_returns.index)] scaled_dm_ps = scaled_dm_ps[scaled_dm_ps.index.isin(scaled_dm_returns.index)]
A scaled dataset used for further analysis would look like the following output: Australia 4/22/99 4/23/99 4/26/99 4/27/99 4/28/99
0.606652 0.609805 0.596189 0.586047 0.590980
Austria 0.414624 0.420241 0.412818 0.410802 0.412811
Belgium 0.616212 0.615688 0.608009 0.605034 0.609483
Canada 0.646860 0.651274 0.642935 0.636934 0.649503
As you can see, the relative order of a variable changes very slowly across time in a country. It is important to remember that one must not use variables that have high volatility to make asset allocation decisions since the implementation cost is very high. When rebalancing an asset allocation portfolio, the trades are typically large in size. Hence, the impact of the market is higher than it would be for strategies that trade single assets. Advances in program trading have reduced the impact cost; however, the relative size of assets is larger.
Building a model A country's index becomes attractive due to a larger increase in its value compared to the values of other country indices. We use the following as measures of the value of a country's index: Relative PE ratio Relative PricetoBook Value (PBV) ratio Relative PS ratio
[ 239 ]
Asset Allocation by LSTM over a CNN
Chapter 7
However, the value of a country's index can remain unappreciated and might not be arbitraged away immediately. But once the momentum begins to build up, the attractiveness increases, and more investors would work towards capturing the return, as can be seen in the following diagram:
Figure 1: Investment model
The diagram in Figure 1 captures the essence of our investment model. It is evident from this that there is a temporal structure where comparative valuations and past attractiveness influence the current level of attractiveness, and a momentum component that is purely along the time dimension. BVAR models coupled with Error Correction (EC) models are wellknown statistical models able to capture this phenomenon. Among them, the most commonly used for asset allocation is the BlackLitterman model. We will adapt the preceding investment model to a deep learning framework to avoid making assumptions about distributional properties, both crosssectionally and for time series.
Understanding the deep learning model Convolutions are a very natural and powerful tool for capturing spatial invariant patterns. By a similar token, convolutions are well suited to modeling recurring patterns such as weekly cyclicality and certain autocorrelation structures. LSTM networks are well suited to forecasting time series data. They are adept at capturing lags of unknown duration between events and reactions in such time series data. Hence, the combined network learns hierarchically, with the later layers in a CNN capturing progressively higherlevel features.
[ 240 ]
Asset Allocation by LSTM over a CNN
Chapter 7
An asset allocation problem has path dependence due to the high cost of switching. In financial terms, the cost to get in and out of a trade is very high. Thus, it might sometimes be beneficial to take a small loss on a position, because the cost of exiting the position can be higher than the temporal loss. Also, the cost to switch is high because of the size of the positions involved. Hence, the problem has a spatial and sequential aspect. This aspect of our problem can be effectively modeled using a CNNLSTM model. A thorough explanation of CNN and LSTM models is covered in Chapter 2, Designing Neural Network Architectures. Essentially, the CNN model works on a panel of data to create a feature matrix, and then these features are used by the LSTM model. Changes in the feature map are then analyzed to predict the attractiveness or unattractiveness of an asset class. This is akin to conducting a panel regression, and then fitting a time series model to the betas (sensitivities) of panel variables across time for decision making. The main drawback of this approach is the assumption of a linear model when fitting a panel regression, and a rigid assumption about the error structure of betas when fitting a time series model. While the preceding disadvantages could be mitigated to a large extent by visual inspection of data and charting sensitivities, the key assumption of linearity cannot be overcome. Also, the result would not give any indication of goodness of fit, for each of the countries. This can be solved by creating one model and fitting it across different country indices, and then, using the models that fit well to make predictions while ignoring the bad models. The model architecture can be diagrammatically represented, as follows:
[ 241 ]
Asset Allocation by LSTM over a CNN
Chapter 7
The CNNLSTM architecture involves using CNN layers for feature extraction on input data. This is combined with LSTMs to predict the sequence of these features. This architecture was originally referred to as a Longterm Recurrent Convolutional Network (LRCN) model. We will, however, use the more generic term CNNLSTM to refer to LSTMs that use a CNN as an input. This architecture is appropriate for problems that have the following: A spatial structure in their input, such as the 1D structure of scaled PE, PB, and PS ratios. A temporal structure in their input, such as the gradual movement across the return attractiveness spectrum, or require the generation of output with a temporal structure, such as progression across the scaled return spectrum. The CNN layer is thus timewrapped in LSTM cells to achieve the time distribution nature of our model. The dense layer at the end of the network provides the output. The LSTM layer builds up an internal state using the Back Propagation Through Time (BPTT) technique to update the weights across a sequence of internal vector representations of the input dataset. Since we are modeling only one country's relative attractiveness per model, we use a 1D CNN with four features, as previously mentioned. Also, our prediction horizon is 22 trading days (that is, 1 month look ahead); hence, we use 20 LSTM layers to make forecasts. So, data older than 22 days is forgotten. This is then combined into a dense layer to determine the output of our model.
Implementing a CNNLSTM model We will use Keras with a Theano backend for our model development, and implementation is in Python 3.6. Since we are fitting a model for every country, we will require datasets to be prepared for every country in order to fit the model iteratively, as follows: 1. Create a dataset of PE, PB, and PS ratios, with the last column containing labels whether to buy or sell, depending upon forward returns to the country index, as follows: # create dataset where the last column is output # split the dataset into 2/3 and 1/3 and make it into training and validation dataset def create_dataset(df1, df2, df3, df4, colidx):
[ 242 ]
Asset Allocation by LSTM over a CNN
Chapter 7
df1 = np.array(df1.iloc[:,colidx]) df1 = df1.reshape((len(df1),1)) df2 = np.array(df2.iloc[:,colidx]) df2 = df2.reshape((len(df2),1)) df3 = np.array(df3.iloc[:,colidx]) df3 = df3.reshape((len(df3),1)) df4 = np.array(df4.iloc[:,colidx]) df4 = df4.reshape((len(df4),1)) dataset = np.hstack((df1,df2,df3,df4,df4)) splitpnt = len(dataset) *2 //3 train_data = dataset[:splitpnt,:] val_data = dataset[splitpnt+1:,:] return train_data, val_data
The preceding function creates the training and validation data, using twothirds of the data for training and onethird of the data for validation. This is important since we need to establish the efficacy of the model before using it for predictions. Training and validation data comes in the form of NumPy arrays of shape (N x 5), where N stands for the number of observations in the training dataset, and 5 denotes the columns of variables in the form: array([[0.6066518 , 0. , 0. , 0.29770505, 0.29770505], [0.60980486, 0. , 0. , 0.25375277, 0.25375277], [0.59618914, 0. , 0. , 0.32067216, 0.32067216], [0.58604696, 0. , 0. , 0.29145003, 0.29145003], [0.59098015, 0. , 0. , 0.29366747, 0.29366747]])
2. Create samples of data for CNN layers to learn the temporal patterns. These will be propagated through the LSTM cells to capture persistence characteristics. The following code snippet shows this: # split a multivariate sequence into samples def split_sequences(sequences, n_steps): X, y = list(), list() for i in range(len(sequences)): # find the end of this pattern end_ix = i + n_steps # check if we are beyond the dataset if end_ix > len(sequences): break # gather input and output parts of the pattern seq_x, seq_y = sequences[i:end_ix, :1], sequences[end_ix1, 1] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y)
[ 243 ]
Asset Allocation by LSTM over a CNN
Chapter 7
The preceding function accomplishes the data prepared in the format of (number of samples X sample size X features).
In our model, we use 22 time steps to determine the temporal pattern since that is in line with the forecast horizon; hence, we do not need to have subsequences for feeding into LSTM cells. Thus, we feed the same sequence into subsequent LSTM cells. 3. Import the relevant Keras modules for model definition, as follows: from from from from from from from
keras.models import Sequential keras.layers import LSTM keras.layers import Dense keras.layers import Flatten keras.layers import TimeDistributed keras.layers.convolutional import Conv1D keras.layers.convolutional import MaxPooling1D
4. Define the hyperparameters and features of the model. Our data has four features, the three valuation ratios, and the past performance of the index, as shown in the following code block: n_steps = 22 n_features = 4 n_seq = 1 # define model model = Sequential() model.add(TimeDistributed(Conv1D(filters=64, kernel_size=1, activation='relu'), input_shape=(None, n_steps, n_features))) model.add(TimeDistributed(MaxPooling1D(pool_size=2))) model.add(TimeDistributed(Flatten())) model.add(LSTM(20, activation='relu')) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')
[ 244 ]
Asset Allocation by LSTM over a CNN
Chapter 7
The model summary is shown in the following code block: model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= time_distributed_1 (TimeDist (None, None, 22, 64) 320 _________________________________________________________________ time_distributed_2 (TimeDist (None, None, 11, 64) 0 _________________________________________________________________ time_distributed_3 (TimeDist (None, None, 704) 0 _________________________________________________________________ lstm_1 (LSTM) (None, 20) 58000 _________________________________________________________________ dense_1 (Dense) (None, 1) 21 ================================================================= Total params: 58,341 Trainable params: 58,341 Nontrainable params: 0 _________________________________________________________________ None
Here are a number of points pertaining to the model: The number of steps is in line with the forecast horizon. Features include valuations that are concurrent and the history of attractiveness. Kernel size is 1 since there is no correlation among the features. 20 LSTM layers are sufficient, and an increase in the number of LSTM layers does not improve model performance. ReLU activation is used since parameters are used for both Conv1D and LSTM. 5. Now, we need to iterate over each of the country indices and valuations over time for the compiled model to be trained. Once trained, the output is examined, to understand if the trained network is good enough to forecast for an individual country index or not. All this is illustrated in the following block of code: for i in range(scaled_dm_returns.shape[1]): t, v = create_dataset(scaled_dm_pe, scaled_dm_pb, scaled_dm_ps, scaled_dm_returns, i) tx, ty = split_sequences(t, n_steps) vx, vy = split_sequences(v, n_steps) tx = tx.reshape((tx.shape[0], n_seq, n_steps, n_features)) vx = vx.reshape((vx.shape[0], n_seq, n_steps, n_features)) filename = scaled_dm_returns.columns[i] +'.json'
[ 245 ]
Asset Allocation by LSTM over a CNN
Chapter 7
history = model.fit(tx,ty,epochs=150, validation_data=(vx, vy), shuffle=False, verbose=0) pyplot.plot(history.history['loss']) pyplot.plot(history.history['val_loss']) pyplot.title('model train vs validation loss') pyplot.ylabel('loss') pyplot.xlabel('epoch') pyplot.legend(['train', 'validation'], loc='upper right') pyplot.savefig((scaled_dm_returns.columns[i] + '.png')) pyplot.close() hist_df = pd.DataFrame(history.history) print(scaled_dm_returns.columns[i] + ' done') with open(filename, mode='w') as f: hist_df.to_json(f) del history del t del v del tx del ty del vx del vy del hist_df
This routine saves the history results for each of the country models and plots for further visual inspection, to determine which of the models can be used for forecasting. The next section reviews the output of the preceding code to determine the efficacy of the model.
Testing and validating our model Having trained the models for each of the country indices, we now need to inspect the training and validation losses in order to segregate models that can be used for prediction and those that cannot. Here, an asset allocation problem differs from a trading problem in two ways: At the aggregate level, different factors that are not quantitative in nature are an influence, such as politics, general risk aversion, and so on. This influence is muted at the individual stock level. Data available relative to the dimensionality of the problem is scarce.
[ 246 ]
Asset Allocation by LSTM over a CNN
Chapter 7
We will inspect the output of each model's training and will look to employ models where the training losses are low and validation losses are not erratic. Variability in validation losses signifies a poor outofsample performance.
Analyzing country models We will analyze each country's model for its usefulness as a predictor of future returns: Singapore: As you can see from the following screenshot, while training losses reduce considerably, the model performs poorly on validation data. Hence, we should reject this model:
[ 247 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Australia: Consistently improving training results, but equally bad validation results. Our model has no information power, as can be seen in the following screenshot:
Austria: Both training and validation losses are erratic. Though the errors are small, it is the volatility of these errors that is the problem. This implies there are factors other than the ones we considered that might improve the accuracy of this model. In its current form, it has no explanatory power and seems to be missing some cyclical factors. The model can be seen in the following screenshot:
[ 248 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Belgium: The model is a good fit, but the validation error has some spikes. This model could be used for a minimal bet as a risk mitigation strategy, and can be seen in the following screenshot:
Canada: Training losses reduce over epochs, and validation losses—though volatile initially—stabilize. The model has some explanatory power and could be used as an adjunct forecast. The model can be seen in the following screenshot:
[ 249 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Denmark, Finland, France, Israel, Italy, Japan, Singapore, Sweden: These models show cases of overfitting. Training losses are reducing but validation losses are increasing. No predictive ability is displayed by these models, which are all shown here:
[ 250 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Germany, Hong Kong, the Netherlands, Norway: Very strong models. Training losses are reducing, and validation losses show no spikes but display stability. This model is definitely the one to use, with a high degree of confidence in forecasts. One could take outsized bets using these forecasts. The models are all shown here:
[ 251 ]
Asset Allocation by LSTM over a CNN
Chapter 7
Ireland, Portugal, Spain, Switzerland: These models are weak, with erratic training and validation losses. They have very low to no predictive power and need to be discarded. The models are all shown here:
United Kingdom: A good fit of the model on training data. High validation losses, but they subside and reduce considerably. This is a useful model for prediction, and can be seen in the following screenshot:
[ 252 ]
Asset Allocation by LSTM over a CNN
Chapter 7
USA: A good fit of the model on training data. While validation losses increase slightly, it could be useful as a model to take small bets, or as an adjunct model. The efficacy of the model in relation to the size and complexity of the market merits consideration. The model can be seen in the following screenshot:
[ 253 ]
Asset Allocation by LSTM over a CNN
Chapter 7
As we can see from the preceding results, 7 of the estimated 21 models are good enough to make predictions. While this can be discouraging to the average reader, it is a very good result for the problem at hand. Being able to bet on 7 out of 21 countries, including some countries with big weights in the index—UK, USA, Hong Kong—is an advantage over peers working with statistical models. The reason for having chosen to predict the order of attractiveness versus the value of an absolute forecast return is clear from the preceding results. Here, we need to know whether the asset class where the model is efficient is good or bad, and then bet accordingly. However, forecasting an absolute return necessitates the forecast of all absolute returns and assumes all models are as effective. For example, knowing that a country index is likely to be in the top quintile enables one to bet on that index, irrespective of the other models not producing any forecast. However, predicting that the return would be 2% does not enable one to bet, since the return structure for all other indices is unknown. Asset allocation is a relative bet problem.
Summary This chapter explored in detail one model—CNNLSTM—being fitted to multiple country indexes, resulting in varying levels of efficacy for different country indices. This illustrated how a single model might not suffice for all the indices, and it would be prudent to explore different models for different indices. We found the information provided by purely quantitative data insufficient in predicting returns to indices. Alternate data sources will be required to factor in qualitative information in a timely manner. The next chapter will explore the use of unstructured data in the form of news flow as a factor influencing returns. This can be incorporated into the asset allocation framework as an additional significant input.
[ 254 ]
8 Digesting News Using NLP with BLSTM The use of alternative and unstructured data sources beyond traditional sources, such as stock market prices, is gaining wide popularity, and the industry is currently investing many resources into it. One of the most popular sources of alternative financial data is the sentiment analysis of news and social posts by using natural language processing (NLP) techniques. This chapter will cover market sentiment analysis by processing a financial news feed using advanced techniques of NLP based on DL. In this case, we will develop a bidirectional LSTM (BLSTM) model by using the Keras deep learning library running on top of TensorFlow. In addition, we will discuss different methods for word embedding that are useful in representing text data. This chapter will cover the following topics: Sentiment analysis for finance Representing text data – words to vectors Data loading and splitting Implementing the BLSTM model Improving performance Considering separate decisions using a complex model
Digesting News Using NLP with BLSTM
Chapter 8
Sentiment analysis for finance NLP is a branch of AI that allows computers to analyze human (natural) language in order to solve tasks such as speech recognition, machine translation, and natural language understanding. Early NLP models were based on handwritten rules. Then, the adoption of ML allowed more complex models to be applied that exploited the available amounts of data. Nowadays, DL models are stateoftheart in many NLP tasks. If you are curious about NLP and possible applications using Python and DL models, you can check out some book references on NLP such as, Natural Language Processing Fundamentals, HandsOn Natural Language Processing with Python, and Deep Learning for Natural Language Processing. Sentiment analysis consists of applying NLP methods to identify subjective information in text data. The sentiment is the opinion of a person with respect to a topic and is usually classified as positive, negative, or neutral. In other words, sentiment analysis is a form of classifying texts according to their emotional polarity. The EfficientMarket Hypothesis (EMH) is an investment theory that states that market prices fully reflect all available information. This means that news has a significant impact on stock market prices. Good news usually induces the market to raise, while bad news tends to bring down the prices. In recent years, research studies have concluded that the public's mood through social media posts, especially from Twitter, also influences stock market movements. Therefore, these alternative data sources can strongly support the decisionmaking process of buying or selling a particular stock. On the other hand, the numerous news articles that are released per day and the rapid development of social networks contain a vast amount of financial information that is impossible to analyze manually. In finance, not only it is important to make effective decisions but it is also important to make them as quickly as possible in order to maximize their profitability. DL can help us with that. We can train DL algorithms on financial news headlines, articles, and social media posts to provide sentiment scores that can be used to generate better trading decisions automatically. A common application of sentiment analysis in finance is for trading individual stocks or even market indexes. For example, iSENTIUM provides a daily sentiment indicator based on tweets to produce a buy/sell signal on the S&P 500 index. Sentiment data can also be applied to build a portfolio. Bloomberg has proposed several strategies to this using the sentiment score. For instance, the Russell 2000 companies proposed to buy the top third in the sentiment ranking and shell those in the bottom, updating the portfolio at the end of each day. In the next section, we will illustrate how to deal with the problem of representing unstructured text data so that it can be managed by algorithms.
[ 256 ]
Digesting News Using NLP with BLSTM
Chapter 8
Representing text data – words to vectors In NLP, we need to map words into vectors of real numbers so that algorithms can handle them. These realnumber vectors represent words from a vocabulary. We can use any set of numbers as a word vector, but it should capture information about semantics, morphology, and the context/relationship between terms in order to be useful. In this section, we'll discuss different ways to represent text data, that is, through the use of frequencybased word vectors and word embeddings.
Frequencybased word vectors Traditional methods for transforming words into vectors are based on the frequency with which each word occurs in a document.
Count vectorization The most basic method, BagofWords (BoW), consists of counting the number of times that each word occurs in each document belonging to a corpus. The size of the text representation (that is, the feature space dimension) corresponds to the number of words in the vocabulary. Therefore, the columns of the resulting matrix represent words and the rows represent documents. In linguistics and NLP, corpus refers to a set of texts or documents. The vocabulary is the set of all unique words in a corpus.
Let's illustrate the BoW transformation with an example. In the following code, we're creating a corpus made up of two sentences and applying the BoW method to them using the scikitlearn library in Python: from sklearn.feature_extraction import text import pandas as pd corpus = [ 'Sales have risen in other export markets', 'Sales increased due to growing market rates and increased operations'] # CountVectorizer vectorizer = text.CountVectorizer() X = vectorizer.fit_transform(corpus)
[ 257 ]
Digesting News Using NLP with BLSTM
Chapter 8
# Creates a pandas DataFrame X_df = pd.DataFrame(data=X.toarray(), \ columns=sorted(vectorizer.vocabulary_.keys())) # View resulting DataFrame X_df.head()
In the preceding code, we used the CountVectorizer method from the sklearn.feature_extraction module by keeping the default parameters. This makes all the words lowercase and removes singleword characters. Then, we created a pandas DataFrame with the resulting transformation in order to visualize it using the pandas .head() function. As shown in the following table, the resulting matrix has two rows that correspond to the two sentences and 15 columns since the number of unique words in the corpus is 15:
Onehot encoding is a similar representation but uses binary values instead of the count term. This is a more common transformation method than BoW since the number of occurrences of each word is usually irrelevant. The following code shows how to apply onehot encoding to the previous corpus: # Onehot encoding vectorizer = text.CountVectorizer(binary=True) X = vectorizer.fit_transform(corpus) # Creates a pandas DataFrame X_df = pd.DataFrame(data=X.toarray(), \ columns=sorted(vectorizer.vocabulary_.keys())) # View resulting DataFrame X_df.head()
In this case, we invoked the CountVectorizer method by passing the binary=True argument in order to set all non zero counts to 1.
[ 258 ]
Digesting News Using NLP with BLSTM
Chapter 8
The following table illustrates onehot encoding for our example:
One issue with counting words is that the most frequent words (such as 'the', 'a', and so on) will appear many times in all the documents but they will not be too meaningful in the word vectors. Let's introduce an alternative.
TFIDF vectorization This method is based on the Term Frequency–Inverse Document Frequency (TFIDF) statistic, which takes into account the occurrence of a word in a single document in comparison to its occurrence in the whole corpus. This is aimed to emphasize words that appear less frequently compared to common words (such as "the", "is", and "this") that appear quite frequently but do not carry as much information. The TFIDF numerical statistic is defined as follows:
In the preceding equation, is the term frequency that can be calculated as the number of times that a term occurs in a document divided by the number of words in the document:
refers to the inverse document frequency, which measures how much information a term provides:
Here, is the total number of documents in the corpus and including the term.
[ 259 ]
is the number of documents
Digesting News Using NLP with BLSTM
Chapter 8
TFIDF method is also available in the sklearn.feature_extraction module through the TfidfVectorizer method. The following code snippet illustrates its application regarding the preceding example: # TFIDF vectorizer vectorizer = text.TfidfVectorizer() X = vectorizer.fit_transform(corpus) # Creates a pandas DataFrame X_df = pd.DataFrame(data=X.toarray(), \ columns=sorted(vectorizer.vocabulary_.keys())) # View resulting DataFrame X_df.head()
The results for TFIDF word vectorization are as follows:
Frequencybased word vectors are easy to compute and are effective for several NLP tasks, but they exhibit several issues. These representations do not state any relationship between words and they are invariant with respect to the word order. In addition, vectors are sparse since a single document usually contains a small group of words in the vocabulary, so vectors are mostly made of zeros. They are also highdimensional vectors, especially for a large corpus, since the feature space dimension depends on the vocabulary size. We need better word representations to solve these issues for our NLP task.
Word embeddings A word embedding creates a mapping from the large vector space, where every unique word entails a single dimension, to a much lowerdimensional space. Therefore, they are dense vectors, as opposed to the sparse vectors that are created by frequencybased methods.
[ 260 ]
Digesting News Using NLP with BLSTM
Chapter 8
In addition, word embeddings are learned from data and each dimension can be seen as a latent factor that encodes semantic information from the corpus that it is trained on. Thus, word embeddings allow similar words to be close in the latent vector space. For this reason, they are known as distributed word representations. A typical example to illustrate this semantic relationship between words has been taken from Mikolov et al. (2013), where we can demonstrate that a welldefined embedding model exhibits gender relation as follows:
Take a look at the following link for more details: https://www.aclweb.org/anthology/ N131090. There are various recent algorithms that can be used to create word embeddings from a large corpus. The most popular methods include the Word2Vec algorithm from Google, FastText, which was created by Facebook, and GloVe, from a team at Stanford University. We'll go over these in the following subsections.
Word2Vec Word2Vec is a group of algorithms that take into account the context of words in order to produce distributed representations. They use a feedforward network in a supervised learning environment to generate the word embeddings. Word2Vec includes two architectures: Continuous Bag of Words (CBOW) and skipgram. Let's briefly describe both methods.
CBOW The CBOW model predicts the current word based on a window of context words around it. As in BoW, prediction is invariant with respect to the order of context words.
[ 261 ]
Digesting News Using NLP with BLSTM
Chapter 8
The following diagram illustrates the CBOW architecture:
In the preceding CBOW model architecture, the inputs are the word vectors of the context words on each side of the target word. From here, the sum of the context word embeddings is taken. The output layer consists of a softmax layer that predicts the word vector of the current word.
Skipgram The skipgram architecture is similar to CBOW, but it swaps the input and output layers instead. The aim of skipgram is to predict the context given by a word. Then, the model learns the probability of a word that appears in a certain context based on another word in the same sentence.
[ 262 ]
Digesting News Using NLP with BLSTM
Chapter 8
The following diagram shows the skipgram architecture:
It has been proven that skipgram performs better for infrequent words but that it is also more computationally complex.
FastText FastText is an extension of Word2Vec that was developed by Facebook. It aims to speed up and outscale the previous method but match its performance. FastText takes into account the morphology of words by splitting them into several ngrams to feed the neural network and adding them at the end to obtain a whole word. By doing this, the model produces word embeddings for all the ngrams in the corpus, as opposed to words. Therefore, infrequent words can be also well represented since their ngrams are likely to be included in other words.
[ 263 ]
Digesting News Using NLP with BLSTM
Chapter 8
GloVe Global Vectors (GloVe) is an unsupervised learning algorithm. The model is trained on the cooccurrence matrix, which represents the frequency with which different words occur simultaneously in a given corpus. It does this by minimizing the least squares error. As a result, it produces a word vector space with linear substructures that captures the relationship between words. So far, we've looked at different ways to represent text data, including frequencybased word vectors (BoW, onehot encoding, and TFIDF) and word embeddings (Word2Vec, FastText, and GloVe). In the next section, we'll introduce the open source dataset that will be used for the experiments in this chapter.
Data loading and splitting The experiments in this chapter involve the public domain phrase bank dataset, created by Malo et al. (2013). It consists of nearly 5,000 sentences from stockmarketrelated news and has been manually annotated as positive, negative, or neutral by several people. They prepared four alternative datasets based on the degree of agreement among the annotators: Sentences that have an agreement of 100% Sentences that have an agreement of more than 75% Sentences that have an agreement of more than 66% Sentences that have an agreement of more than 50% In our experiments, we will restrict our analysis to the last dataset. The full dataset is available at https://www.researchgate.net/publication/251231364_ FinancialPhraseBankv10. Of course, you can also find it in this book's GitHub repository at https://github.com/PacktPublishing/HandsOnDeepLearningforFinance. Let's get started: 1. The following code snippet is aimed at loading the dataset and showing the first five rows. We use the pandas and numpy libraries to do this: import pandas as pd import numpy as np ## Load dataset df = pd.read_table('FinancialPhraseBankv1.0/Sentences_50Agree.txt',
[ 264 ]
Digesting News Using NLP with BLSTM
Chapter 8
delimiter='\r\n') values = np.array([df.values[i][0].split('@') for i in range(df.size)]) data = pd.DataFrame({'sentence':values[:, 0], 'sentiment':values[:, 1]}) # Show first rows data.head()
Here's the output:
Our sentiment analysis problem contains three classes to be predicted: neutral, negative, and positive. It is convenient to explore the target variable in order to find out their distributions through the classes. 2. The following code plots the target distribution and shows the percent that's relative to each class. We use the matplotlib.pyplot module to do this: import matplotlib.pyplot as plt # Visualize the target distribution ax = data['sentiment'].value_counts().plot(kind='bar') ax.set_ylabel('Percentage of sentiments', fontsize=12) ax.set_yticks(np.arange(0, 3501, 500)) # Print percents per class total = data['sentiment'].size for i in ax.patches: ax.text(i.get_x() + i.get_width()/2., i.get_height() + 40, str(round((i.get_height()/total)*100, 2)) + '%', ha ='center', fontsize=15) plt.show()
[ 265 ]
Digesting News Using NLP with BLSTM
Chapter 8
The target distribution of the dataset under analysis can be seen in the following graph:
Here, we can see that the classes are strongly imbalanced. We should take this peculiarity into account when training the model. 3. The next step is to split the loaded data into training and testing sets in an 80/20 split: from sklearn.model_selection import train_test_split ## Split dataset into training and testing sets X = data.sentence y = data.sentiment X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=7)
The preceding code shows the data being split using the train_test_split() method from sklearn.model_selection. The arguments of this method are test_size=0.2, stratify=y, and random_state=7. stratify=y is used to the split data in a stratified fashion, while random_state=7 is used to fix the seed for simulations.
[ 266 ]
Digesting News Using NLP with BLSTM
Chapter 8
Now, let's move on to talking about the necessary steps for implementing the deep learning model for sentiment analysis of financial news.
Implementing the BLSTM model In this section, we'll discuss how to create, fit, and evaluate the model for sentiment analysis by using the dataset we referred to previously. The model is based on a bidirectional LSTM (BLSTM) architecture and we use word embeddings to represent the text data. The following is a simple diagram of an unrolled BLSTM architecture:
BLSTM consists of two connected LSTM layers with the same input in opposite directions; that is, the first LSTM layer takes the input sequence as it is, whereas the second one receives a reversed copy of the input. The outputs of these two layers are then merged at each time step, so it will depend on the internal state of both LSTM layers. This provides contextual information in both directions, which is useful for sequence classification problems where the context of a time step plays an important role.
[ 267 ]
Digesting News Using NLP with BLSTM
Chapter 8
Data preparation In this section, we'll prepare the input and target data so that they're ready for modeling. In NLP, we need to clean and prepare the raw text data before fitting any model. On the other hand, we will use word embeddings that map word indexes into dense vectors and are able to encode semantic information from a corpus. For this reason, they only require minimal text cleaning, which usually includes removing all punctuation and converting words into lowercase. Let's go over this now: 1. The following code snippet shows how to tokenize our text data using the Tokenizer() class from keras.preprocessing.text. This class assigns an integer to each word in the corpus, where each integer represents the index of a token in a dictionary. We invoke the constructor of this class without arguments, that is, using the default values. The important arguments are num_words=None, which considers all the words in the corpus, filters='!"#$%&£()*+,./:;?@[\]^_`{}~', which removes all punctuation, and lower=True, which sets all words to lowercase. Note that we fit Tokenizer() with the training data and then apply it to the training and testing sets to mimic a real situation: from keras.preprocessing.text import Tokenizer ## Tokenize training and testing sets tokenizer = Tokenizer() tokenizer.fit_on_texts(X_train) X_train = tokenizer.texts_to_sequences(X_train) X_test = tokenizer.texts_to_sequences(X_test)
In computer science, tokenization is the process of breaking up a sequence of characters into a sequence of tokens. A token is a piece of a sequence of characters that represents a unit with meaning, such as words, keywords, or phrases. In the process of tokenization, certain characters, such as punctuation, are removed. 2. The tokenizer object contains the word_index attribute, which is a dictionary that associates each word in the vocabulary with the corresponding index. We save this dictionary into an independent variable: word_index = tokenizer.word_index
[ 268 ]
Digesting News Using NLP with BLSTM
Chapter 8
3. Finally, we pad the sequences with zeros to ensure that all the sequences have the same length when feeding the network. To do this, we use the pad_sequences() function from keras.preprocessing.sequence. We fix the length to the maximum sequence length related to the training set. This can be done because our sentences aren't too long. In fact, the maximum length is 71 tokens: from keras.preprocessing.sequence import pad_sequences # Pad the sequences max_len = np.max([len(X_train[i]) for i in range(len(X_train))]) X_train = pad_sequences(X_train, maxlen=max_len) X_test = pad_sequences(X_test, maxlen=max_len)
4. The target data also needs to be prepared since labels are categorical. We transform labels into onehot encodings in a twostep process. First, we encode the labels as integers using the LabelEncoder() class from sklearn.preprocessing. Then, we convert the integers into onehot encodings by means of the to_categorical() function from keras.utils.np_utils. This following code makes this transformation: from sklearn.preprocessing import LabelEncoder from keras.utils.np_utils import to_categorical # Encode target values as integers le = LabelEncoder() le.fit(y_train) y_train = le.transform(y_train) y_test = le.transform(y_test) # Convert integers to one hot encoded y_train = to_categorical(y_train) y_test = to_categorical(y_test)
In addition, we define a variable that represents the number of classes (or sentiments) so that we can use it later: n_classes = y_train.shape[1]
The encoded vectors we constructed in this section can now be used directly by any algorithm.
[ 269 ]
Digesting News Using NLP with BLSTM
Chapter 8
Creating and fitting the model Once we have prepared our data, we are ready to define and train the BLSTMbased network. We feed the model with sequences of equal length made up of word indexes. The produced target is a onehotencoded vector that indicates the predicted sentiment of the regarded sequence. The model is composed of a first embedding layer that transforms the sequences of word indexes into dense vectors of a fixed size. This layer will learn the word embeddings, along with the network parameters, during the training phase. Then, a BLSTM layer is aimed at finding out about the relationships among the embedded inputs and providing features that are useful for the final classification. Its output is passed through a fully connected dense layer in order to reduce the dimensionality of the feature space. The output dense layer provides the final decision about the sentiment. The following scheme depicts the model:
General scheme of the model
Follow these steps to create and fit a model: 1. First, we import the necessary libraries to define the model: from keras.models import Sequential from keras.layers import Embedding, Dropout, Bidirectional, LSTM, GlobalMaxPool1D, Dense
2. Then, we set the dimension of the embedding to 300 since this is a typical value. This is a hyperparameter that we can control in our experiments: # Embedding size EMBEDDING_DIM = 300
[ 270 ]
Digesting News Using NLP with BLSTM
Chapter 8
3. Next, we define the sequential model by calling the .Sequential() method, as follows: ## Create the model model = Sequential()
4. The embedding layer is created using the Keras Embedding() class. Its constructor takes the size of the vocabulary as an argument that corresponds to the maximum integer index plus 1, the dimension of the word embeddings, and the length of the input sequence. After this, we add a dropout layer with 0.25 as the rate of inputs to drop in order to reduce the chance of overfitting: model.add(Embedding(len(word_index)+1, EMBEDDING_DIM, input_length=max_len)) model.add(Dropout(0.25))
5. Then, we define the bidirectional layer since it provides a bidirectional wrapper for RNNs (Bidirectional()). The inputs to the Bidirectional() constructor are the LSTM layer, which is made of 200 units, and the mode to combine the outputs of the forward and backward LSTM layers (merge_mode). We set merge_mode='concat' so that the output values will be concatenated. You can visit Keras (https://keras.io/layers/wrappers/#bidirectional) to learn about other merge options. The LSTM layer is required to return the full sequence, and is done by setting return_sequences=True. On the other hand, if the LSTM layer only returns the last output in the sequence, the backward LSTM layer will only be able to see the last time step of the input sequence and the BLSTM won't work properly. We also add a dropout layer after this. Let's have a look at the following implementations: model.add(Bidirectional(LSTM(200, return_sequences=True), merge_mode='concat')) model.add(Dropout(0.25))
6. The output of the BLSTM consists of a 3D tensor with a shape of (batch_size, steps, features). Since our problem is not a sequencetosequence problem, we add a GlobalMaxPool1D() layer. This creates a pooling by taking the maximum value over the steps dimension. The output of this layer is a 2D tensor with a shape of (batch_size, features): model.add(GlobalMaxPool1D())
[ 271 ]
Digesting News Using NLP with BLSTM
Chapter 8
7. Subsequently, we add a Dense() layer made up of 50 units that uses the sigmoid activation function. After this fully connected layer, we will include a dropout layer: model.add(Dense(50, activation='sigmoid')) model.add(Dropout(0.25))
8. The next step is to add the output layer. This is a Dense() layer that has as many neurons as the number of classes we want to identify. The activation function that's being used here is the softmax function. It is suitable for multiclass classification problems. Binary classification is the problem of classifying data samples into one of two classes. In multiclass classification, the instances are classified into one of three or more groups. In multilabel classification, more than one label may be assigned to each instance. Let's take a look at the following code: model.add(Dense(n_classes, activation='softmax'))
9. After defining the network, it's time to compile it by applying the .compile() method to the BLSTM model, as we mentioned previously. We use crossentropy as the loss function because our problem consists of classifying sequences. Specifically, we set loss='categorical_crossentropy' since it is a multiclass classification task. We use the RMSProp optimizer and the accuracy as a metric to evaluate the model during the training and testing phases. The following code is aimed at compiling the model: ## Compile the model model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['acc'])
10. Finally, we are ready to train the model by using the .fit() method, as shown in the following code: ## Train the model batch_size = 64 epochs = 10 model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0)
[ 272 ]
Digesting News Using NLP with BLSTM
Chapter 8
We've set up the batch size to be 64 as this value will give us a tradeoff between compilation time per epoch and model quality due to its ability to generalize. We've fit the network for 10 epochs.
Evaluating the model Now that we've trained the BLSTM model, it's time to evaluate it using the testing dataset. We can use different metrics to assess the model's performance when evaluating a sentiment analysis problem and, more generally speaking, a classification problem. A common performance metric to consider is the accuracy. To calculate it, we can use the .evaluate() method, which returns the loss value and the metric that we imposed during the compilation, that is, the model accuracy. This method uses the input test data, the target test data, and the batch size, since the computation is done in batches, as arguments. The classification accuracy is the percentage of correct predictions, that is,
. The following code shows us how to obtain the model classification accuracy considering the testing set: ## Compute the testing accuracy test_loss, test_acc = model.evaluate(X_test, y_test, batch_size=batch_size) print("Testing accuracy: {:.1f}".format(test_acc * 100))
The output of the preceding code is as follows: Testing accuracy: 77.3%
Our model is able to predict the sentiment of unseen sentences with 77.3% accuracy, which means that it provides around 77% correct predictions. This is not a bad result considering that the task of sentiment analysis is very difficult due to aspects such as lack of context, subjectivity, or sarcasm, as outlined by Bing Liu (2010). It is known that, in general, humans disagree with each other numerous times in their daily life, usually about 20% of the time. We have seen that the annotators, agreement of the dataset that we are considering is pretty low. For this reason, the acceptable accuracy for sentiment analysis algorithms is around 80%.
[ 273 ]
Digesting News Using NLP with BLSTM
Chapter 8
But is the accuracy the best way to decide how well our classification model works? The answer is no. This measure does not give us information about the distribution of predictions over the classes. However, this aspect is important in classification tasks, especially when classes are largely imbalanced, as they are in our case. For instance, if we consider a problem where one class is predominant and the model gives the value of this category for all predictions, then we obtain a misleading high accuracy. This is known as the accuracy paradox. Thus, the classification accuracy is not a reliable metric for measuring the performance in such cases and we need better measures to evaluate the classifier. The confusion matrix is a tool that allows us to visually understand the correct predictions and the errors that are made by our classifier. The following table represents the confusion matrix for a binary classification problem:
In the preceding table, each row represents the instances in every real class, while each column contains the instances in every predicted class. The diagonal elements correspond to the number of samples for which the predicted label is the same as the true label, whereas the rest of the elements represent the misclassified samples. Then, the confusion matrix reports the number of true positives, false negatives, false positives, and true negatives. These values can be used to calculate precision and recall. Precision measures the number of correct predictions over all positive predictions. It indicates the exactness of the model and can be defined as follows:
Recall is the number of positive predictions over all the positive instances in the test set. It indicates the completeness of the model and can be written as follows:
The F1 score is another classification measure that combines both precision and recall. The F1 score is the harmonic average of the precision and recall:
[ 274 ]
Digesting News Using NLP with BLSTM
Chapter 8
We can easily obtain the confusion matrix, precision, recall, and F1 score of our BLSTM model using the module metrics from scikitlearn. Before that, we need to generate predictions with our network for the test dataset. To do this, we can use the .predict() method, as we did in the previous chapters: ## Make predictions y_pred = model.predict(X_test, batch_size=batch_size)
As output, the model gives us a vector of probabilities stating that a certain sentence will belong to each class. This is happening because the output layer uses the softmax function. We should transform the resulting threedimensional array into a onedimensional array containing the predicted classes. This can be done by taking the index of the maximum probability. We do this using the numpy.argmax() function. We do the same with the test data because it is onehotencoded: y_test = np.argmax(y_test, axis=1) y_pred = np.argmax(y_pred, axis=1)
The confusion matrix can be calculated using the confusion_matrix() function from sklearn.metrics, as follows: from sklearn.metrics import confusion_matrix # Confusion matrix cm = confusion_matrix(y_test, y_pred) # Set options to print 2 decimals np.set_printoptions(precision=2)
To visualize the confusion matrix, we create a figure based on the code from scikitlearn. You can find this at https://scikitlearn.org/stable/auto_examples/model_ selection/plot_confusion_matrix.html#sphxglrautoexamplesmodelselectionplotconfusionmatrixpy.
The function that's used to plot the confusion matrix takes the confusion matrix, the name of the classes, the title of the figure, and the colormap to be used as arguments. The code for this is as follows: import itertools def plot_confusion_matrix(cm, classes, title='Confusion matrix', cmap=plt.cm.Blues): """ This function prints and plots the confusion matrix. """ plt.imshow(cm, interpolation='nearest', cmap=cmap)
[ 275 ]
Digesting News Using NLP with BLSTM
Chapter 8
plt.title(title) plt.colorbar() tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=45) plt.yticks(tick_marks, classes) fmt = '.2f' if normalize else 'd' thresh = cm.max() / 2. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\ plt.text(j, i, format(cm[i, j], fmt),\ horizontalalignment="center", color="white" if cm[i, j] > thresh else "black") plt.ylabel('True label') plt.xlabel('Predicted label') plt.tight_layout()
The following code snippet calls the preceding function in order to plot the confusion matrix. Before that, we take the name of the labels from the LabelEncoder object to print them correctly: labels = le.classes_.tolist() # Plot confusion matrix plt.figure() plot_confusion_matrix(cm, classes=labels)
Here is the resulting diagram:
[ 276 ]
Digesting News Using NLP with BLSTM
Chapter 8
As we can see, the majority of correct predictions are obtained for neutral sentences. This is normal since it is the predominant class and the model was trained to minimize the overall error. For this reason, the misclassified negative and positive phrases are mostly classified as neutral. The following piece of code is aimed at calculating the precision, recall, F1 score, and support of each class using the precision_recall_fscore_support() function from sklearn.metrics. Then, we print the results using a pandas DataFrame: The support is the number of instances belonging to each class.
from sklearn.metrics import precision_recall_fscore_support # Precision, recall, f1 score and support p, r, f1, s = precision_recall_fscore_support(y_test, y_pred, average=None) results = pd.DataFrame({'1Precision': p, '2Recall': r, \ '3F1 score': f1, '4Support': s}, index=labels) # Print precision, recall, f1 score and support print(results.round(decimals=3))
The following is the output: negative neutral positive
1Precision 0.664 0.806 0.736
2Recall 3F1 score 0.587 0.623 0.870 0.837 0.652 0.691
4Support 121 575 273
The low precision value of negative sentences indicates a high percentage of predicted negatives that are not negative, whereas the low recall score tells us that the model is unable to correctly detect all the negative phrases. The same occurs for positive sentences, although with slightly better performance. However, the model is able to detect almost every neutral sentiment, as indicated by a high recall value, and it is quite exact in predicting them. The F1 score is quite good for neutral sentences, but not for negative and positive ones.
[ 277 ]
Digesting News Using NLP with BLSTM
Chapter 8
Improving performance In the previous section, we created and fitted a BLSTM model to predict the sentiment of financial news. The preliminary results aren't very good, so let's try to improve the quality of our predictions.
Dealing with imbalanced classes As we mentioned previously, the main issue with our example is class imbalance. The predominance of neutral sentences leads to a biased model that tends to classify many instances as neutral. Different strategies exist to deal with an imbalanced training set. The most simple way is to balance it by sampling the data. This can be done by oversampling elements of the minority class or undersampling elements of the majority class. The oversampling method consists of randomly adding copies of the elements from a dataset. The undersampling method involves randomly remove elements from a dataset. How should we sample our training dataset? We should be undersampling neutral elements and oversampling instances of the negative class to give both of them the same support as the positive class. Why? Because neutral instances are numerous and we are not as interested in them, and we also have just a few negative instances. An easy method for sampling our dataset is by using the resample module from scikitlearn, with replacement for oversampling and without replacement for undersampling. The corresponding code must be placed after you've split the dataset. Let's go over the steps: 1. First, we need to separate instances of each class into different DataFrames, like so: from sklearn.utils import resample, shuffle ## Sampling the dataset # Separate classes neu_index = y_train[y_train == 'neutral'].index pos_index = y_train[y_train == 'positive'].index neg_index = y_train[y_train == 'negative'].index
[ 278 ]
Digesting News Using NLP with BLSTM
Chapter 8
2. Then, we resample the neutral class without replacement, setting the number of samples to the number of positive samples. We do the same with the negative classes but, in this case, the resample is done with replacement: # Downsample neutral class X_train_neu, y_train_neu = resample(X_train.loc[neu_index], y_train.loc[neu_index], \ n_samples=len(pos_index), replace=False, \ random_state=7) # Upsample negative class X_train_neg, y_train_neg = resample(X_train.loc[neg_index], y_train.loc[neg_index], \ n_samples=len(pos_index), replace=True, \ random_state=7)
3. After that, we add the resampled classes to the positive class DataFrame: # Combine resampled classes X_train_resample = X_train.loc[pos_index].append([X_train_neu, X_train_neg]) y_train_resample = y_train.loc[pos_index].append([y_train_neu, y_train_neg])
4. Finally, we shuffle the instances by using the shuffle() method from sklearn.utils: # Shuffle samples X_train_resample, y_train_resample = shuffle(X_train_resample, y_train_resample, random_state=7)
5. Let's look at the new distribution of data: # Display new class counts print(y_train_resample.value_counts())
The following is the output: positive 1090 neutral 1090 negative 1090 Name: sentiment, dtype: int64
[ 279 ]
Digesting News Using NLP with BLSTM
Chapter 8
6. Now, we are ready to fit the BLSTM model again using this balanced training dataset. Before that, we need to repeat the data preparation steps using X_train_resample and y_train_resample. The model accuracy is now as follows: Testing accuracy: 73.2%
The resulting confusion matrix is as follows:
Here, we can see that the number of correct predictions for the negative and positive classes are more numerous than they are when using the dataset as it is. However, the number of neutral sentences that were detected correctly is lower than it was previously. For this reason, the classification accuracy is also lower. The precision, recall, F1 score, and support are as follows: negative neutral positive
1Precision 0.609 0.835 0.616
2Recall 3F1 score 0.694 0.649 0.750 0.790 0.711 0.660
[ 280 ]
4Support 121 575 273
Digesting News Using NLP with BLSTM
Chapter 8
The precision values for negative and positive sentences are lower than before, indicating more incorrect predictions when assigning these labels. On the other hand, the recall is higher, pointing out that the model is able to detect more sentences of these categories. Precision for neutral sentiments is slightly higher, but recall in this case is lower. Unfortunately, it is not possible to maximize both precision and recall concurrently, so we need to find a tradeoff. In our case, both metrics are important. High precision for negative and positive sentences is needed because making incorrect decisions can lead us to lose money if we were to use the classifier within a trading strategy. On the other hand, we are interested in obtaining a high recall in order to maximize our possible revenues. One solution can be to try to maximize the F1 score.
Applying pretrained word embeddings Up until now, we have used an embedding layer so that the word embeddings were learned jointly with the main task of sentiment classification during training, the same as the network parameters. Instead of this approach, it is possible to load and use precomputed word embeddings, which are known to exhibit suitable characteristics in the latent space. In this section, we will use the pretrained word embeddings we'll be creating using the Word2Vec, FastText, and Glove algorithms. Let's start by using Word2Vec embeddings in a Keras model. The pretrained model was computed using a corpus of around 100 billion words from a Google News dataset. It contains 300dimensional word vectors for a vocabulary of 3 million words. You can download the model from https://code.google.com/archive/p/word2vec/. In order to load the pretrained model in your own machine, you need the gensim library, which is available in PyPI and can be installed via pip (pip install gensim). The following piece of code shows you how to load the word vectors from Google's model using the gensim library. You need enough available memory in your computer to load such a huge matrix: import gensim # Load Google's pretrained Word2Vec model word2vec = gensim.models.KeyedVectors.load_word2vec_format("GoogleNewsvectorsnegative300. bin", binary=True)
[ 281 ]
Digesting News Using NLP with BLSTM
Chapter 8
All the pretrained word vectors will be loaded, but we don't need all of them. We just need those that correspond with words in our vocabulary. Therefore, we will define a function that will create the embedding matrix that we will use. This function uses the loaded model and the word_index variable containing our vocabulary as arguments. Then, it associates the word embeddings from the Word2Vec model to the words in the dictionary. Note that any words that are not found in the pretrained embeddings are set to vectors of all zeros. The following code defines such a function: def word2vec_matrix(model, word_index): """ This function creates an embedding matrix from Word2Vec word embeddings. """ embedding_matrix = np.zeros((len(word_index) + 1, EMBEDDING_DIM)) for word, i in word_index.items(): if word in model.index2word: embedding_matrix[i] = model[word] return embedding_matrix
Now, we can create the embedding matrix by calling the preceding function. After that, we need to save it to a binary file by using the save() method from NumPy, as follows: w2v_matrix = word2vec_matrix(word2vec, word_index) np.save('emb_matrix_w2v', w2v_matrix)
Now, we are ready to train our BLSTM model using the pretrained word embeddings. The only thing that we must change when defining the model is the embedding layer. We need to load the embedding matrix into the Embedding layer. Note that we set trainable=False so that the word vectors aren't updated during training: model.add(Embedding(len(word_index)+1, EMBEDDING_DIM, weights=[word2vec_matrix], \ trainable=False, input_length=max_len))
The new model is compiled and trained for 50 epochs. Here's the model accuracy: Testing accuracy: 79.6%
The confusion matrix is as follows:
[ 282 ]
Digesting News Using NLP with BLSTM
Chapter 8
The precision, recall, F1 score, and support are as follows: negative neutral positive
1Precision 0.721 0.874 0.692
2Recall 3F1 score 0.769 0.744 0.809 0.840 0.780 0.733
4Support 121 575 273
Overall, we have obtained better results. However, the precision values should be higher for negative and positive sentences. Similarly, we can use other pretrained word embeddings. You can directly download the FastText word vectors from https://fasttext.cc/docs/en/englishvectors.html. It's a .zip file named wikinews300d1M.vec.zip and contains 300dimensional word vectors for 1 million words that have been trained on 16 billion tokens from several corpora, including Wikipedia 2017. Glove's pretrained word vectors can be found at https://nlp.stanford.edu/projects/glove/. This is also a .zip file called glove.6B.zip and contains a 300dimensional vocabulary of 400,000 word vectors that have been trained on 6 billion words from Wikipedia 2015 and other sources. In both files, each line contains a word followed by the corresponding vector, separated by a space character.
[ 283 ]
Digesting News Using NLP with BLSTM
Chapter 8
In this case, the function that's used to create the word embeddings works in two steps. First, we compute an index that's been mapping between words and known embeddings by parsing the loaded data. Then, we create our embedding matrix by associating words in our vocabulary with their word vectors. Here's the code for this: def embedding_matrix(fname, word_index): """ This function creates an embedding matrix. """ f = open(fname, 'r', encoding='utf8', newline='\n', errors='ignore') vectors = {} for line in f: tokens = line.rstrip().split(' ') vectors[tokens[0]] = np.asarray(tokens[1:], dtype='float32') f.close() embedding_matrix = np.zeros((len(word_index) + 1, embed_dim)) for word, i in word_index.items(): embedding_vector = vectors.get(word) if embedding_vector is not None: embedding_matrix[i] = embedding_vector return embedding_matrix
Next, we can generate our embedding matrices for the FastText and Glove embeddings and save them: # Load and save FastText word vectors ft_matrix = embedding_matrix('wikinews300d1M.vec', word_index) np.save('emb_matrix_ft', ft_matrix) # Load and save Glove word vectors glove_matrix = embedding_matrix( 'glove.840B.300d.txt', word_index) np.save('emb_matrix_glove', glove_matrix)
The training accuracy that's obtained by training the BLSTM using fixed FastText embeddings is as follows: Testing accuracy: 80.0%
[ 284 ]
Digesting News Using NLP with BLSTM
Chapter 8
Here's the output matrix:
The rest of the measures are as follows: negative neutral positive
1Precision 0.776 0.848 0.713
2Recall 3F1 score 0.744 0.760 0.842 0.845 0.737 0.724
4Support 121 575 273
The testing accuracy of using glove vectors is as follows: Testing accuracy: 78.8%
[ 285 ]
Digesting News Using NLP with BLSTM
Chapter 8
The corresponding confusion matrix is as follows:
The precision, recall, F1score, and support are as follows: negative neutral positive
1Precision 0.733 0.842 0.701
2Recall 3F1 score 0.702 0.717 0.842 0.842 0.714 0.708
4Support 121 575 273
By looking at all of the preceding results, we can conclude that using pretrained word vectors provides a much better model performance than learning the embeddings together with the main task. This may be because we don't have enough training data to learn useful embedding weights. However, pretrained word vectors are trained with billions of words from different corpora and they can generalize quite well in our collection of financial news phrases. The Word2Vec model tends to provide better recall for negative and positive sentences, whereas FastText seems to offer better precision scores. When using Glove word embeddings, we obtained poorer results.
[ 286 ]
Digesting News Using NLP with BLSTM
Chapter 8
Considering separate decisions In the previous section, we learned how to deal with the problem of class imbalance. This issue makes it difficult to correctly detect negative and positive sentences since they are less frequent than neutral ones. What about considering separate decisions? In this section, we will train two independent models to solve this issue. One of the models will be aimed at distinguishing between neutral and nonneutral sentences, while the other will be trained to distinguish between negative and positive sentences. Then, we will combine them into a more complex model to make the final decision. Note that training these models requires preprocessing our training dataset in a different way. Let's start with the model aimed at discriminating positive sentiments from negative ones. To get the data for this model, we need to remove neutral sentences from the dataset. Then, we need to split the dataset and oversample the negative class to balance the new dataset. Finally, we need to combine the positive and negative classes and shuffle the instances, as we have already seen. You can find the relevant code in this book's GitHub repository: https://github.com/PacktPublishing/HandsOnDeepLearningforFinance. Then, we need to apply the data preparation steps. Note that we won't refit the tokenizer object so that we have the same word index and thus the same embedding matrix for all models. We set max_len=71 for the same reason, and also for defining all the models with the same input shape. The target values will be encoded as integers since this is a binary classification. The following BLSTM model has the same architecture as the previous one, except that the output layer is made by one single unit and uses the sigmoid activation function. We'll use the Word2Vec embeddings we loaded in the previous section. The following code defines the model. Note that we name the layers; we'll explain why later: # Create the model model = Sequential() model.add(Embedding(len(word_index)+1, embed_dim, weights=[w2v_matrix], trainable=False, \ input_length=max_len, name='posneg_embedding_1')) model.add(Dropout(0.25, name='posneg_dropout_1')) model.add(Bidirectional(LSTM(200, return_sequences=True), merge_mode='concat', \ name='posneg_blstm_1')) model.add(Dropout(0.25, name='posneg_dropout_2')) model.add(GlobalMaxPool1D(name='posneg_pool_1'))
[ 287 ]
Digesting News Using NLP with BLSTM
Chapter 8
model.add(Dense(50, activation='sigmoid', name='posneg_dense_1')) model.add(Dropout(0.25, name='posneg_dropout_3')) model.add(Dense(1, activation='sigmoid', name='posneg_dense_2'))
After creating the model structure, we compile it using the binary crossentropy (loss='binary_crossentropy') as the loss function and then we fit the network for 40 epochs, as follows: # Compile the model model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['acc']) # Train the model model.fit(X_train2, y_train2, epochs=40, batch_size=batch_size, shuffle=True)
We evaluate the model in terms of precision, recall, and F1 score using the following code: ## Make predictions y_pred = model.predict(X_test, batch_size=batch_size) y_test = np.round(y_test) y_pred = np.round(y_pred) # Precision, recall, f1 score and support p, r, f1, s = precision_recall_fscore_support(y_test, y_pred, average=None) # Print precision, recall, f1 score and support results = pd.DataFrame({'1Precision': p, '2Recall': r, \ '3F1 score': f1, '4Support': s}, index=labels) print(results.round(decimals=3))
Here's the output of the preceding code: negative positive
1Precision 0.886 0.929
2Recall 3F1 score 0.835 0.860 0.952 0.940
4Support 121 273
This model performs well. It is able to detect negative and positive sentiments quite effectively. It's possible that the predictions are better for positive sentences because they are more numerous.
[ 288 ]
Digesting News Using NLP with BLSTM
Chapter 8
The final step is to save the model. In Keras, this involves saving the model architecture and the model weights separately. First, we convert the structure of the model into a JSON string using the .to_json() method. Then, we save it to a file in the local directory: # Serialize model to JSON model_name = 'posneg_model' model_json = model.to_json() with open(model_name + '.json', 'w') as json_file: json_file.write(model_json)
The network weights are saved as an HDF5 file using the .save_weights() method. Note that you may need to install the h5py library that's available in PyPI (pip install h5py): # Serialize weights to HDF5 model.save_weights(model_name + '.h5')
Similarly, we continue by implementing the model that's dedicated to distinguishing between neutral sentences and nonneutral ones. In this case, to get the training data, we need to change the labels accordingly. We assign 1 to neutral sentiments and 0 to the others. Then, we oversample the nonneutral class, combine both classes, and shuffle the instances, just like we did previously. The code that makes this transformation can be found in this book's GitHub repository: https://github.com/PacktPublishing/HandsOnDeepLearningforFinance. After that, we apply the data preparation steps, as we did previously. We don't refit the tokenizer object and use max_len=71 again. In this case, we do not need to transform the class labels. The model we're using here is the same as the previous one, but it is assigning different names to the layers. Keras assigns default name layers if a name is specified. In such a case, the layers of both models would have the same names. This would lead to an error when we combine both architectures into a more complex one later. After training the model for 40 epochs, we obtain the following results: 1Precision 2Recall 0.761 0.815 0.867 0.824
nonneutral neutral
3F1 score 4Support 0.787 394 0.845 575
[ 289 ]
Digesting News Using NLP with BLSTM
Chapter 8
The model is able to distinguish between neutral and nonneutral sentences quite well. Now, we need to save this architecture and their learned weights: # Serialize model to JSON model_name = 'neu_model' model_json = model.to_json() with open(model_name + '.json', 'w') as json_file: json_file.write(model_json) # serialize weights to HDF5 model.save_weights(model_name + '_1str.h5')
Now, we are ready to create a more complex architecture in order to solve the initial problem of sentiment analysis on financial news data. Such a network combines the two previous models in order to detect neutral, negative, and positive sentiments. The first step consists of loading the two models we trained previously. In the following code, we are doing the following: 1. First, we load the JSON file that contains the architecture specifications. 2. Then, we create a model from it by using model_from_json(). 3. The learned weights should be loaded using the .load_weights() function: from keras.models import model_from_json # POSNEG model # Load JSON and create model posneg_model_name = 'posneg_model' posneg_json_file = open(posneg_model_name + '.json', 'r') loaded_posneg_model_json = posneg_json_file.read() posneg_json_file.close() posneg_model = model_from_json(loaded_posneg_model_json) # Load weights into the model posneg_model.load_weights(posneg_model_name + '.h5') # POSNEG model # Load JSON and create model neu_model_name = 'neu_model' neu_json_file = open(neu_model_name + '.json', 'r') loaded_neu_model_json = neu_json_file.read() neu_json_file.close() neu_model = model_from_json(loaded_neu_model_json) # Load weights into the model neu_model.load_weights(neu_model_name + '.h5')
[ 290 ]
Digesting News Using NLP with BLSTM
Chapter 8
The complex network combines these two models by concatenating their outputs. It does this by using the Merge layer with mode='concat'. Their output is passed through a Dense layer made of 10 units. It uses the sigmoid function to do this. The output layer has three neurons and uses the softmax activation function. We include a Dropout layer with a rate equal to 0.1: from keras.layers import Merge ## Create complex network model = Sequential() model.add(Merge([posneg_model, neu_model], mode='concat', name='merge_1')) model.add(Dense(10, activation='sigmoid', name='complex_dense_1')) model.add(Dropout(0.1, name='complex_dropout_1')) model.add(Dense(y_train.shape[1], activation='softmax', name='complex_output'))
Before compiling and fitting the complex model, we need to freeze the layers of the pretrained models to avoid their weights being updated during training. To do this, we need to set the trainable attribute of each layer to False, as follows: # Freeze the layers of pretrained models for layer in posneg_model.layers: layer.trainable = False for layer in neu_model.layers: layer.trainable = False
Then, we can compile and train the network on the balanced data we created in Dealing with imbalanced classes section. As this model has two different input layers – one for each pretrained model – we must feed the network with two inputs by passing a list. The same must be done for making predictions. The following piece of code is aimed at compiling and training the model for 50 epochs: # Compile and train model model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['acc']) # Train model model.fit([X_train_resample, X_train_resample], y_train_resample, epochs=epochs, batch_size=batch_size, shuffle=True, verbose=1)
[ 291 ]
Digesting News Using NLP with BLSTM
Chapter 8
Finally, we evaluate the model and make predictions on it. After that, we compute the confusion matrix, precision, recall, and F1 score, like so: # Compute the testing accuracy test_loss, test_acc = model.evaluate([X_test, X_test], y_test, batch_size=batch_size) print("Testing accuracy: {:.1f}".format(test_acc * 100)) # Make predictions y_pred = model.predict([X_test, X_test], batch_size=batch_size)
The classification accuracy is as follows: Testing accuracy: 83.3%
Here's the confusion matrix:
[ 292 ]
Digesting News Using NLP with BLSTM
Chapter 8
The other classification metrics are as follows: negative neutral positive
1Precision 0.783 0.873 0.769
2Recall 3F1 score 0.777 0.780 0.875 0.874 0.769 0.769
4Support 121 575 273
As you can see, we've improved the model's performance even further using a complex architecture.
Summary In this chapter, we created and trained a BLSTMbased model to classify sentences from stockmarketrelated news as positive, neutral, or negative. First, we discovered how to process text data and how to represent words in a distributed fashion through word embeddings. Then, we learned how to implement a BLSTM model in Keras using the Embedding layer to obtain the vector representation of words. We have seen how to learn word vectors while fitting the whole network and how to adopt pretrained embeddings. Since the considered dataset is strongly imbalanced, we discussed how to solve this issue by using a resampling approach and how to correctly evaluate the model using suitable metrics. Finally, to improve the model's performance, we created a complex model whose main building blocks are two independent models: one of them was trained to distinguish between positive and negative sentences, whereas the other model was trained to distinguish between neutral and nonneutral sentiments. The results were very encouraging, since our final model was able to correctly predict the sentiment most of the time. One important advantage of this model is that it is not lexicondependent, which means it can be easily applied to another corpus. In addition, text data does not require complex preprocessing to be managed. In the next chapter, we will investigate how to select factors for alpha prediction by using a generative adversarial network (GAN).
[ 293 ]
9 Risk Measurement Using GAN Risk and return are the foundations of finance. Most of this book has focussed on the one aspect that gets the most publicity—returns. This is what is most interesting to us; however, an important aspect of returns is risk. Counterintuitive, but true. This chapter will focus on the use of generative adversarial networks (GANs) for estimating the variance–covariance matrix and using it for Monte Carlo simulations. Here, we deviate from traditional Monte Carlo methods—we will estimate the general nonlinear function of the variance–covariance structure and generate a new matrix for every simulation. This is more robust than the current methods. As well as this, we will have a look at the following topics: Estimating value at risk (VaR) Introducing generative adversarial networks Implementing a risk model using a GAN Benchmarking results
Estimating value at risk Risk measurement and management are very important. Anyone, having seen the financial results of an investment bank, would have come across sections where it says that the firm lost money for only three days in the last quarter. This aspect of the result is as important as the return on equity or return on riskweighted assets reported by the bank. This signifies the potential for losses that could turn the ink to red from black. Investment banks, therefore, invest a lot in riskmanagement divisions, which often report to the Chief Financial Officer, thereby giving them a direct voice to the board of directors. Risk committees proliferate at the bank, often equal in number to investment committees. The past few crises in the markets have solidified this reputation and increased investments of resources into riskmanagement divisions.
Risk Measurement Using GAN
Chapter 9
One of the most important measures of risk is called value at risk (VaR). Value at risk is a measure of the risk of potential losses for a portfolio of assets. It is defined as the maximum amount that the portfolio is expected to lose over a given time period at a predefined confidence level. From the preceding definition, if the reported value of 99% onemonth VaR is $5 million, it means that the portfolio will not lose more than $5 million over one month, 99% of the time.
Computing methods and drawbacks VaR can be calculated using two different techniques: the parametric and nonparametric method. With the parametric method, also known as the variance–covariance method, VaR is calculated as a function of the variance and covariance of the returns series. Some continuous distribution is assumed (joint normal) in most cases, or a combination of continuous distribution to account for nuances in financial time series data. This procedure requires us to make an assumption about the joint distribution of future returns. Past returns data is used to estimate the parameters of this joint distribution. The estimated function is then used to forecast the variance–covariance matrix, which is then multiplied by the portfolio weights to compute the parametric VaR. Advances in parametric methods include the estimation of joint distributions using asymmetric copulas; however, the proliferation of assets and paucity of data make estimations unreliable, and so the forecasting is questionable. The financial data being inherently noisy also tends to make this estimation difficult. The Monte Carlo method simulates large numbers of price evolution scenarios for the portfolio. The returns for each of these scenarios are computed and a return distribution of portfolios is constructed from this. VaR is then computed using the return at the confidence level desired from this distribution of returns. There is a large body of literature that focusses on these aspects but it is outside the scope of this chapter. The nonparametric method is also called the historical returns method. In this method, VaR is determined by taking the returns belonging to the tertile associated with the confidence level desired and taking note of the highest of those returns. Here, no assumptions are made about distributions and no statistical techniques are used; however, one assumption is that the future will be exactly the same as the past. This might be true for limited time horizons, but given the jumps in financial time series, even this assumption is one to be circumspect about. Despite being widely used, VaR has many shortcomings:
[ 295 ]
Risk Measurement Using GAN
Chapter 9
Firstly, VaR quantifies the potential loss at a certain probability level, but there is no estimate of the size of the loss associated with the tail of the probability distribution up to this level; therefore, it does not account for the fatness of the tails or the asymmetry in the fatness of the tails. For example, a position might have a VaR estimate of $1 million at a 95% confidence interval, implying that the position will not lose more than $1 million 95% of the time; however, one would not know if the distribution probability for losses exceeds this amount. Secondly, VaR is not additive—that is, the VaR of the overall portfolio is not a linear combination of the VaR of the components of a portfolio, since VaR takes into account correlations, and linear combinations could lead to double counting. As a result, VaR decomposition is tricky. When simplistic assumptions are being used about all assets following the same distribution with a stable correlation structure, the results of the portfolio VaR would have a wide confidence interval. For example, if we combine two assets, such as a sovereign bond and gold, the distributions of both of the assets are not normal, and the correlation structure would have jumps rather than a smooth multinormal distribution. In the case of a crisis, both the sovereign bond and gold will move in sync because of risk aversion; however, in a normal situation, a sovereign bond will be governed more by local factors, and gold by global factors. Lastly, different calculation methods give different results. Different estimation methods give different results, not only because of the difference in techniques, but also the different data that is being considered. This means that, when computing with, say, threemonth data just after a financial crisis, VaR would be higher than realized, while VaR would be lower than realized if we are computing after a bullish period. This stems from the statistical tradeoff—more data is equal to more noise and so generates ineffective statistics. This is why most practitioners use a blend of techniques and average the estimates to get a better likelihood. Is there a way of capturing all the features of the dataset without having to rely on neat equations? The answer to this question is to use neural networks to define and estimate the nonlinear equation set. Statistical methods rely on first fixing the method and then estimating the parameters, while neural networks carry out both processes simultaneously. Generative adversarial networks achieve this elegantly. One could think of it in terms of a reverseengineering process, but with minimal constraints. The rest of this chapter will focus on the use of GAN to estimate the value at risk model.
[ 296 ]
Risk Measurement Using GAN
Chapter 9
Introducing generative adversarial networks What is the best way to master any skill? Learn the skill. Use the skill. Get feedback on what is still missing. Relearn the missing skill. Unlearn things that are not contributing towards the skill. Get feedback. Keep repeating this cycle until you have become proficient. This is exactly how GANs work, as shown here:
GAN is a deep learning, unsupervised machine learning technique proposed by Ian GoodFellow and other researchers in 2014.
[ 297 ]
Risk Measurement Using GAN
Chapter 9
A few points about GAN: In GAN, we have a generator network that is pitted against an adversarial network called a discriminator, hence the name generative adversarial network. Both generator and discriminator are multilayer perceptrons (MLP). The generator network’s objective is to model or generate data that is very similar to the training data. The generator needs to generate data that is indistinguishable from the real data. Generated data should be such that the discriminator finds it difficult to identify it as real data. The discriminator network's objective is to identify whether the data is real or fake. It gets two sets of input: one input comes from the training dataset and the other input is the modeled dataset generated by the generator.
Generative models Generative models are constructed to learn the probability distribution by attempting to fool the discriminator model that the fake data is real. This can be achieved only if the fake data belongs to a random sample from the same joint distribution. Here are some other things to bear in mind about generative models: Generative models learn the joint probability P(x,y) of the input data x and output data y. They make predictions based on P(xy). Given output label y, they reconstruct input x. Given some data, they identify the latent feature representation. What are the features that can help identify the presence of cancer in a patient? Generative models can help reconstruct the input data. Generative models learn the distribution of individual classes. They function well on outliers. Generative modeling can generate new data points from the sample data. Some examples of generative models include naive Bayes, restricted Boltzmann machine, and deep belief nets.
[ 298 ]
Risk Measurement Using GAN
Chapter 9
Discriminative models The function of a discriminator network is a binary classification. The discriminator network can also be thought of as the objective function that is being optimized when training a generator network. Here are some other things to bear in mind about discriminative models: Discriminative algorithms model the conditional probability, P(yx). They do this without making any assumptions about the input distribution. A discriminative classifier learns the boundary between the classes explicitly. Given the data, the discriminative model predicts the class to which the particular data belongs. Given a set of features, such as age, family history, weight, and whether the person smokes or not, a discriminative model can predict whether the person is likely to have cancer. Discriminative models do not perform well on outliers One example of a discriminative model is decision trees. SVMs are discriminative algorithms.
Inner workings of GAN The way the GAN is trained is by setting up the generator network and discriminator network. The roles and workings of these individual networks are described as follows: Generator: Input to the generator is random noise created from the training data. Training data can be an image or any multidimensional dataset. The generator tries to mimic the input dataset as closely as possible to the real dataset from the training data. The generator’s goal is to fool the discriminator. Discriminator: The discriminator gets two inputs: one is the real data from the training dataset and the other is the fake data from the generator. The goal of the discriminator is to identify which input is real and which is fake. This makes the GAN ideal for our problem of the identification of distributions for assets by extracting features from the historical dataset. MLPs are great at noise reduction and feature extraction. This is the equivalent of fitting a distribution to the variance–covariance matrix, which can be used to simulate return paths and compute VaR estimates from them.
[ 299 ]
Risk Measurement Using GAN
Chapter 9
Training a generator model is akin to fitting a distribution without minimal assumptions. Generating time series from this model is akin to random sampling from a fitted joint distribution of returns.
Implementing a risk model using GAN GANs have been extensively employed in the field of image processing with exciting and encouraging results. An image is a twodimensional representation of data with some structure between data points. A variance–covariance matrix is similar to an image where every column or row can be thought of as price or return history of an asset class, and the matrix encapsulates a structure that is present within those returns.
Defining our model Images can be represented as a two or threedimensional array of numbers where the first two dimensions represent pixels on the X–Y plane and the third dimension encodes the color of those pixels. A multivariate time series can be thought of as a grayscale image—that is, an array that has only two dimensions. Let's build a GAN to study our problem. Say we have a set of 50 stocks with a daily history of over 25 years. The following facts pertain to our scenario and the exercise: We should introduce a lot of noise to the system, simulating realworld scenarios. We should incorporate four crisis periods where markets had very large falls–that is, the crisis of 1993–1994, the Asian Crisis of 1998, the Internet Crash of 2001, and the Housing Bubble of 2008. The chosen stocks belong to different industries, and so the impacts would be felt differently for different crises. Some crises would have a direct impact while others would be beneficial. The same analysis could be extended to a larger number of assets and different types, but the basic idea remains the same.
[ 300 ]
Risk Measurement Using GAN
Chapter 9
Both the models are CNN models. The reason for using both models as CNN models is the pooling layer. The pooling layer in the CNN model removes positional dependence in the structure. Once we have a good generator model, we would like to create a sample time series. This sample time series preserves the structure of a univariate series, but combinations could be markedly different from historical observations, so the joint distribution does not need to fit exactly.
Implementing the GAN model The GAN model will be implemented by going through the following steps using Keras with TensorFlow as the backend: 1. Import all of the libraries that will be required for preprocessing and fitting the network. We will use the TensorFlow backend, but the results should not differ for the Theano backend as well: import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline import random from sklearn.preprocessing import minmax_scale import h5py from tabulate import tabulate Import keras functions import os os.environ['KERAS_BACKEND'] = 'tensorflow' import keras from keras.layers import Dense, Dropout, Input from keras.models import Model,Sequential from keras.datasets import mnist from tqdm import tqdm from keras.layers.advanced_activations import LeakyReLU from keras.optimizers import adam from keras.models import load_model
2. Now we need to import the time series data and put it in a structure that is readable to our model. You can parameterize the function to select the stocks that are desired rather than reading the first 50 of the list. The following code snippet does this: def create_dataseries(): symlist = pd.read_csv('nasdaq.csv') # this would change depending upon parameterization symlist = symlist[0:50]
[ 301 ]
Risk Measurement Using GAN
Chapter 9
data = pd.DataFrame() # now read each file and align the data along the date axis for i in range(len(symlist)): filename = 'csvdata/' + symlist.Ticker[i] + '.csv' temp = pd.read_csv(filename, index_col = ['Date'],usecols=['Date','Close']) temp = temp.loc[~temp.index.duplicated(keep='first')] temp.columns = [symlist.Ticker[i]] temp = temp.shift(1)/temp 1 temp = temp[:1] if(len(temp) > len(data)): data = pd.concat([temp, data],axis =1, sort=False) else: data = pd.concat([data, temp],axis =1, sort=False) data = data.fillna(0) return data
3. To create the test and training datasets, we use a rolling window to capture the transient effects in our dataset: Create training samples # samplesize determines the number of observations to use # rollwindow creates a loop after skipping a few data points def create_samples(samplesize, rollwindow): # get the raw data series which is return series dataset = create_dataseries() samples = list() for i in range(0,len(dataset)samplesizerollwindow,rollwindow): end_ix = i + samplesize if end_ix > len(dataset): break temp = dataset[i:end_ix] # now scale this matrix by the largest value being 1 and lowest value being 1 #temp = minmax_scale(temp, feature_range=(1,1),axis=0) samples.append(temp.values) return np.array(samples)
[ 302 ]
Risk Measurement Using GAN
Chapter 9
4. Since we are interested in daily returns, we will create pictures with a onemonth horizon—that is, 22 trading days—and try and learn the features for those pictures. Samples are reshaped to give the number of samples and the size of the picture for each sample: samples = create_samples(22, 1) samples = samples.reshape(samples.shape[0], samples.shape[1]*samples.shape[2])
From this, we get the following output: samples of shape (number of samples X 1100 )
Now let's define the generator, which takes a random noise of 100 and upsamples it to an image of 1100: def adam_optimizer(): return adam(lr=0.0002, beta_1=0.5) def create_generator(): generator=Sequential() generator.add(Dense(units=256,input_dim=100)) generator.add(LeakyReLU(0.2)) generator.add(Dense(units=512)) generator.add(LeakyReLU(0.2)) generator.add(Dense(units=1024)) generator.add(LeakyReLU(0.2)) #generator.add(Dense(units=2048)) #generator.add(LeakyReLU(0.2)) #generator.add(Dense(units=5096)) #generator.add(LeakyReLU(0.2)) generator.add(Dense(units=1100, activation='tanh')) generator.compile(loss='binary_crossentropy', optimizer=adam_optimizer()) return generator g=create_generator() g.summary()
[ 303 ]
Risk Measurement Using GAN
Chapter 9
The model will be as follows:
The discriminator similarly takes the images and determines whether they are fake or not. The discriminator network is initially trained with a batch of labeled fake and real images. Once trained, these weights are then kept fixed and the predict function of the discriminator model is fed with fake images from the generator network to identify how many of the images were correctly classified as fake. Since we know that all of the images are fake, the fewer the number of images that are classified as fake, the better our generator network is at approximating the joint distribution: def create_discriminator(): discriminator=Sequential() discriminator.add(Dense(units=1024,input_dim=1100)) discriminator.add(LeakyReLU(0.2)) discriminator.add(Dropout(0.3))
discriminator.add(Dense(units=512)) discriminator.add(LeakyReLU(0.2)) discriminator.add(Dropout(0.3)) discriminator.add(Dense(units=256)) discriminator.add(LeakyReLU(0.2)) discriminator.add(Dense(units=1, activation='sigmoid'))
[ 304 ]
Risk Measurement Using GAN
Chapter 9
discriminator.compile(loss='binary_crossentropy', optimizer=adam_optimizer()) return discriminator d =create_discriminator() d.summary()
When both of these are combined, they give us the GAN model, as shown here: def create_gan(discriminator, generator): discriminator.trainable=False gan_input = Input(shape=(100,)) x = generator(gan_input) gan_output= discriminator(x) gan= Model(inputs=gan_input, outputs=gan_output) gan.compile(loss='binary_crossentropy', optimizer='adam') return gan gan = create_gan(d,g) gan.summary()
This model can then be trained as shown in the following code. The training function defines the model and its hyperparameters. During the training process, weights are estimated for the generator model so that the fake data generated by the model cannot be discriminated by the discriminator model: def training(epochs=1, batch_size=10): #Loading the data X_train = samples batch_count = X_train.shape[0] / batch_size # Array initialization for logging of the losses d_loss_logs_r = [] d_loss_logs_f = [] g_loss_logs = [] # Creating GAN generator= create_generator() discriminator= create_discriminator() gan = create_gan(discriminator, generator)
[ 305 ]
Risk Measurement Using GAN
Chapter 9
The following code block illustrates how generator and discriminator networks are trained. This iterative process is as follows: 1. Random noise is fed to the generator network, that has been initialized with random weights. 2. The generator network generates some images—that is, twodimensional data or a scaled variance–covariance matrix. This data generated from this is labeled as fake data. 3. A set of real data is then taken from the training data and variables are initialized with the real and fake data and their corresponding labels to be used during training: # We can train the network for a number of epochs for e in range(1,epochs+1 ): print("Epoch %d" %e) for _ in tqdm(range(batch_size)): #generate random noise as an input to initialize the generator noise= np.random.normal(0,1, [batch_size, 100]) # Generate fake images from noised input generated_images = generator.predict(noise) y_fake = np.zeros(batch_size) # Get a random set of real images image_batch =X_train[np.random.randint(low=0,high=X_train.shape[0],size=batch_s ize)] y_real = np.ones(batch_size) #Construct different batches of real and fake data X= np.concatenate([image_batch, generated_images]) # Labels for generated and real data y_dis=np.zeros(2*batch_size) y_dis[:batch_size]=0.9
The trick in training a GAN is to use the following twostep process: 1. Train the discriminator on real and fake images. 2. Keep the discriminator weights fixed and train the generator network to generate fake images like real images. This is akin to a statespace variable estimation approach:
[ 306 ]
Risk Measurement Using GAN
Chapter 9
Pretrain the discriminator on fake and real data before starting the gan. Now set the trainable flag for the discriminator to true and feed a batch of real and fake images with the corresponding labels. The discriminator will identify the distinctive features of fake images relative to the features of real images: discriminator.trainable=True d_loss_real = discriminator.train_on_batch(image_batch, y_real) d_loss_fake = discriminator.train_on_batch(generated_images, y_fake) #d_loss_comb = discriminator.train_on_batch(X, y_dis) d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
The GAN is trained by feeding the discriminator images of generated data as real data: noise= np.random.normal(0,1, [batch_size, 100]) y_gen = np.ones(batch_size)
The pretrained discriminator is now fixed with the trainable flag set to False: discriminator.trainable=False
GAN is trained by alternating the training of the discriminator and training the chained GAN model with the discriminator’s weights frozen: g_loss = gan.train_on_batch(noise, y_gen) print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (e, d_loss_real, 100*d_loss_fake, g_loss)) # Store the losses d_loss_logs_r.append([e, d_loss_real]) d_loss_logs_f.append([e, d_loss_fake]) g_loss_logs.append([e, g_loss]) # end of training d_loss_logs_r_a = np.array(d_loss_logs_r) d_loss_logs_f_a = np.array(d_loss_logs_f) g_loss_logs_a = np.array(g_loss_logs)
The stored losses need to be analyzed further by visual inspection for the following: Speed of convergence: The larger number of heterogeneous assets should require a slow speed of convergence Presence of local optima: This is in case the losses show periodicity
[ 307 ]
Risk Measurement Using GAN
Chapter 9
We can see this by looking at the following code: #At the end of training plot the losses versus epochs plt.plot(d_loss_logs_r_a[:,0], d_loss_logs_r_a[:,1], label="Discriminator Loss  Real") plt.plot(d_loss_logs_f_a[:,0], d_loss_logs_f_a[:,1], label="Discriminator Loss  Fake") plt.plot(g_loss_logs_a[:,0], g_loss_logs_a[:,1], label="Generator Loss") plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend() plt.title('GAN') plt.grid(True) plt.show() plt.savefig('GANFIT_1.png') #if e == 1 or e % 20 == 0: #plot_generated_images(e, generator)
We can summarize the composite GAN model as follows: 1. Let's have a look of the generator network in the following block: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_1 (Dense) (None, 256) 25856 _________________________________________________________________ leaky_re_lu_1 (LeakyReLU) (None, 256) 0 _________________________________________________________________ dense_2 (Dense) (None, 512) 131584 _________________________________________________________________ leaky_re_lu_2 (LeakyReLU) (None, 512) 0 _________________________________________________________________ dense_3 (Dense) (None, 1024) 525312 _________________________________________________________________ leaky_re_lu_3 (LeakyReLU) (None, 1024) 0 _________________________________________________________________ dense_4 (Dense) (None, 1100) 1127500 ================================================================= Total params: 1,810,252 Trainable params: 1,810,252 Nontrainable params: 0
[ 308 ]
Risk Measurement Using GAN
Chapter 9
2. You can see the discriminator network that is also an MLP in the following block: _________________________________________________________________ _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_5 (Dense) (None, 1024) 1127424 _________________________________________________________________ leaky_re_lu_4 (LeakyReLU) (None, 1024) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 1024) 0 _________________________________________________________________ dense_6 (Dense) (None, 512) 524800 _________________________________________________________________ leaky_re_lu_5 (LeakyReLU) (None, 512) 0 _________________________________________________________________ dropout_2 (Dropout) (None, 512) 0 _________________________________________________________________ dense_7 (Dense) (None, 256) 131328 _________________________________________________________________ leaky_re_lu_6 (LeakyReLU) (None, 256) 0 _________________________________________________________________ dense_8 (Dense) (None, 1) 257 ================================================================= Total params: 1,783,809 Trainable params: 1,783,809 Nontrainable params: 0 _________________________________________________________________
3. The combination of the preceding generator and discriminator networks is shown in the following block: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 100) 0 _________________________________________________________________ sequential_1 (Sequential) (None, 1100) 1810252 _________________________________________________________________ sequential_2 (Sequential) (None, 1) 1783809 ================================================================= Total params: 3,594,061 Trainable params: 1,810,252 Nontrainable params: 1,783,809 _________________________________________________________________
[ 309 ]
Risk Measurement Using GAN
Chapter 9
The model is trained and the training loss is analyzed for overfitting or local optima. When the discriminator cannot distinguish between fake and real inputs, the training is complete. While this is quite unlikely to occur quickly with larger datasets, since the complexity increases, we can stop training our network once there is no improvement in the generator model's ability to create real data. Remember that the accuracy of a variance–covariance model deteriorates significantly as the time horizon of forecasting increases.
For our example, after training for 100 epochs, the desired result is achieved and the generator and discriminator fitted models are saved. The generator will be used later to generate estimates: training(100,50) g.save('generator_2.h5') d.save('discriminator_2.h5')
The following plot illustrates the goodness of fit:
[ 310 ]
Risk Measurement Using GAN
Chapter 9
The preceding figure shows that the network learns the features from the samples quickly and converges in its accuracy. The oscillation of the generator loss function signals no additional benefit in training the network for more epochs.
Benchmarking results Now that we have fit the model and saved the final generator, we can use it to make predictions using Monte Carlo simulations. We will compare the results with those of historical data to ascertain the efficacy of using this method: 1. Initialize some random portfolio weights and generate random samples: # initialize portfolio weights. Use real weights if available init_weights = np.random.uniform(0,1,50) # portfolio weights must add up to 1 init_weights = init_weights / sum(init_weights) #now generate 100000 samples and compute the VaR for various horizons and save them g = load_model('generator_1.h5') noise= np.random.normal(0,1,[10000,100]) ret_samples = g.predict(noise) ret_samples = ret_samples.reshape(ret_samples.shape[0],22,50)
The value at risk for a portfolio is computed by multiplying the initial weights of the portfolio by the forecast returns. These forecast returns are sampled from fitted distributions when employing statistical models. In our case, the trained generator network acts as a nonlinear distribution from which returns can be sampled for each of the assets. Multiplying these returns by our initial portfolio weights will give us a distribution of forecast returns. Therefore, we apply the Monte Carlo approach but use the generator network for forecast return sampling, rather than a fitted joint distribution model. This forecast distribution is then used to predict the value at risk for different quantiles. In addition to the generator function, it is important to note that the forecast horizon is required since the return distribution comprises cumulative returns of the portfolio. We therefore need a generic function to compute the value at risk. The following function takes the portfolio weights and generated returns to compute the return history for a given time horizon: def compute_port_VaR(port_weights, var_hor, ret_samples): port_returns = []
[ 311 ]
Risk Measurement Using GAN
Chapter 9
for num_samp in range(ret_samples.shape[0]): for ret_hor in range(ret_samples.shape[1]): end_ix = ret_hor + var_hor if end_ix > ret_samples.shape[1]: break temp = ret_samples[num_samp, ret_hor:end_ix,:] if var_hor > 1: temp = np.cumprod((temp + 1), axis =0) temp = np.dot(temp[len(temp)1], port_weights) 1 port_returns.append(temp) else: port_returns.append(np.dot(temp, port_weights)) return np.array(port_returns)
The preceding code takes as input portfolio values and computes the value at risk over the return horizon. We use the method of daily compounding to account for the increased or decreased relevance of individual starting positions and thereby capture the path dependence of the portfolio. 2. Now we compute the VaR for a 1 to 21 day period since our sample size is for 22 days at most: VaRList = list() for i in range(1,21): tempvar = compute_port_VaR(init_weights, i, ret_samples) VaRList.append(tempvar)
The preceding code used daily returns for VaR computation. If the sampling frequency was higher, then the same computation could have been done at a granular level. This would be useful for highfrequency trading strategies. 3. Using the following code determines the VaR values for upper and lower tails: e = 1 for v in VaRList: temp = np.sort(v,axis=None) f_VaR5 = temp[(int(len(v)*0.05))] f_VaR95 = temp[(int(len(v)*0.95))] print ("%d [5 VaR: %.2f%%, 99 VaR: %.2f%%]" %(e, f_VaR5*100, 100*f_VaR95)) e = e + 1
[ 312 ]
Risk Measurement Using GAN
Chapter 9
While the connotation of VaR is the amount of money you an lose, it is also important to compute the upper tail VaR to assess whether the fat tails in return distributions of asset classes have been captured. From the results in the following screenshot, it is evident that this is indeed the case. Note the asymmetry between the upper and lower tails:
It would be good to benchmark this against conventional methods of computing VaR. 4. A similar exercise can be performed by getting the raw data and computing the historical VaR using the following code: now compute VaR just using real data dVaRList = list() for i in range(1,21): ret_mat = dataset.shift(1*i)/dataset 1 ret_mat.drop(ret_mat.tail(i).index,inplace=True) tempvar = np.dot(ret_mat.values,init_weights) dVaRList.append(tempvar)
The preceding code block takes the realized historical returns and multiplies the compound returns with initial portfolio weights to arrive at the value at risk estimate. Here, the key assumption is that the future is identical to the past.
[ 313 ]
Risk Measurement Using GAN
Chapter 9
5. Let's plot along the same lines using the following code: e = 1 for v in dVaRList: temp = np.sort(v,axis=None) f_VaR5 = temp[(int(len(v)*0.05))] f_VaR95 = temp[(int(len(v)*0.95))] print ("%d [5 VaR: %.2f%%, 95 VaR: %.2f%%]" %(e, f_VaR5*100, 100*f_VaR95)) e = e+ 1
We get the following results:
We can observe the following from the preceding computations: For the GANgenerated samples, the VaR increases on the upper and lower tails as the forecast horizon increases. This is consistent with the scaling theory of financial risk—that is, the risk increases with the horizon if portfolio weights remain constant. Both upper and lower tail values increase.
[ 314 ]
Risk Measurement Using GAN
Chapter 9
The values generated by GAN are significantly higher than those generated by the historical method. This happens because we are at a period just after a long bull market where stock returns have had big upsides and small downsides. Therefore, the measurement bias is highlighted. GAN, on the other hand, has not forgotten the features of financial crises and so estimates a higher VaR. As we can see from the preceding results, GAN results in better estimates of VaR.
Summary This chapter has focused on an important aspect of investment management—risk estimation. CNN has been applied successfully in discriminating twodimensional data. It can also be used to generate twodimensional or multidimensional data with some structure. GAN models can be used to estimate a generator network, which is equivalent to estimating the joint PDF of returns. This can be used to generate multiple samples, and is used in Monte Carlo simulations for estimating VaR. This chapter leveraged the concept of using conflict to improve the performance of the network we would like to use for forecasting. Here, the reader has learned to set up a GAN model and train it so that the generator network can generate data that is akin to real data. This trained network is then used to generate sample data for Monte Carlo simulations for risk estimation. Monte Carlo simulations are widely used because fitting a joint distribution is not tractable most of the time. Financial time series, though approximated with Gaussian distributions, have fat tails. This leads to behavior in tails that is not easily modeled by the probability density functions of known distributions. The joint estimation for asset classes is further complicated by the joint behavior not being a homogenous process. Estimating a function for joint distribution and the partial distribution of returns is a crucial element for Monte Carlo simulations. The next chapter will work on the concept of incremental improvement by improving performance using pretrained networks, thereby saving computational time and increasing the reliability of results.
[ 315 ]
4 Section 4: Advanced Techniques This section is devoted to introducing some trending topics regarding deep learning in finance. In particular, this part will introduce transfer learning, capsule networks (CapsNets), and reinforcement learning. This section comprises the following chapters: Chapter 10, Chart Visual Analysis by Transfer Learning Chapter 11, Better Chart Analysis Using CapsNets Chapter 12, Training Trader Robots by Deep Reinforcement Learning Chapter 13, What Next?
10 Chart Visual Analysis by Transfer Learning In Chapter 6, Trading Rule Identification by CNN, we developed a Convolutional Neural Network (CNN) model to identify buy and sell signals using technical analysis criteria. Here, the criteria were expressed in numerical terms: the strength of the moving average (MA) indicators and their relative ranking. While technical analysis is mathematically expressed in this way when explaining it to others, practitioners rarely follow principles strictly. A visual analysis through a chart is more important to them. Sometimes, inferences are made because they consider not just point estimates but also historical trends. One could think of this as some sort of fuzzy logic where the main tenet is clear to everyone, but there is other information that is subconsciously (for the lack of a better word) considered. This is rather like how a human being, when seeing a situation, can describe its salient points very accurately when it comes to saying why they chose a certain action; however, there are additional reasons. This chapter aims to develop on this and tries to mimic what the actual trader sees in charts when they make a decision. Hence, rather than inputting numerical inputs for the price series and associated moving averages to CNN as implemented in Chapter 6, Trading Rule Identification by CNN, , we input images and let the network determine the important features.
Chart Visual Analysis by Transfer Learning
Chapter 10
While we could develop another CNN model from scratch, it might be less timeconsuming to use pretrained models and adapt them for our purpose. Here, we explore another feature of machine learning: transfer learning. The rest of the chapter discusses the importance and characteristics of transfer learning, builds a model to apply transfer learning to our problem using the ResNet50 trained network, and flags some ideas for future research. This chapter will cover the following topics: Explaining what transfer learning is Using visual inspection in transfer learning for technical analysis Implementing a transfer learning model Predicting test images
Explaining what transfer learning is The concept of transfer learning is quite simple and elegant. Just as we do not learn everything from scratch but build upon our understanding from other domains, thus speeding up the learning process, so too can we use similar networks and build upon their training rather than training one from scratch. So, when a trader moves from one market to another, they do not learn everything from scratch but look at similarities within the markets, and adapt their experiential database to learn the new setting quickly—hence the need for experienced professionals!! Human beings do not learn everything from scratch but rather apply their learning across different activities. The knowledge we acquire while learning one activity is used in a similar manner to solve related tasks. Thus, the human mind distills the principles from an activity and applies the same principles to other activities that are similar in nature. The more similar two activities are, the easier it becomes for us to crossutilize what we've learned. The following are some examples of this: Trading developed markets is similar whether you trade in US or European markets, due to similarities in institutional structure and participants. Trading in bond and currency markets is similar due to the underlying factors that govern both markets and their liquidity characteristics.
[ 318 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
Most machine learning and deep learning algorithms are designed for specific activities, and they work in isolation. This need not be the case. There is some similarity in problems; whether image classification is in respect of images related to animals or related to weather, at a fundamental level these activities are similar. Hence, algorithms built and trained to solve specific tasks are not required to be rebuilt from scratch after the featurespace distribution changes. Transfer learning is the idea of adapting knowledge acquired for one activity to solve similar problems by minor modifications, just like human beings do. In the statistical domain, this would be akin to Bayesian statistics. This method has been applied to different scenarios in finance. An example would be estimating a multifactor model using data from developed markets such as the US. The model construct and parameters thus estimated are used as priors. New data from emerging markets is then used to update the parameters, with the emerging market data acting as an update function, to arrive at new parameters. Thus, one can reduce the dependence upon data for the initial estimation. Articles about quants using methods from information theory (made famous by Renaissance Technologies and Two Sigma) are akin to transfer learning but use statistical methods that have shown promising results in one domain of signal processing to financial prices. Transfer learning relies on this basic philosophy. A detailed explanation of transfer learning is beyond the scope of this book and there are many excellent sources to learn from. What we will outline is the concept, motivation, and some key features of transfer learning, before working on a real example. The main driver in the context of deep learning is that most models need a lot of data to train, and this may not be possible for all domains, keeping in mind the amount of time and effort it takes to label data, even whether data is available. Also, most deep learning models are specialized and they focus on a particular domain, or even a subdomain. While these might be very complex models with high accuracy of prediction or classification, this would be applicable for certain datasets only, and the performance of these models is likely to deteriorate considerably when applied to a new problem that is similar to the one on which it was trained but that belongs to a different domain and, hence, a different dataset. This is where transfer learning comes to the fore! This goes beyond specific activities and domains, leveraging what has been learned from pretrained models and using this to solve problems in a new domain. For this to work, the two problems must be similar in nature.
[ 319 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
Understanding transfer learning The differences between traditional machine learning and transfer learning are more than superficial. The following screenshot illustrates these differences:
Transfer learning leverages knowledge (features, weights, and so on) from models that have been trained earlier on data in order to train other models for problems with less data than was the case when the initial model was trained. This is akin to traditional quantitative model development where models are developed on markets that have better data quality to arrive at a good statistical model, and then use the same model to determine the new weights for different markets. The development of stock selection models on US markets and then applying these to emerging markets is a wellknown practice. In the field of computer vision, certain lowlevel features such as edges, shapes, corners, and intensity are common across different types of problem. A human face, an animal face, a grass blade, and a price chart all have these features. These can be applied across different tasks, and can thus take advantage of knowledge transfer. We can formally define transfer learning as follows:
[ 320 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
Given a source domain , a corresponding source task , as well as a target domain and a target task , the objective of transfer learning is to enable us to learn the target conditional probability distribution in with the information gained from the source domain and task. Here, the source domains and target domains are different, and generally the target examples are smaller than the source examples. This brings us to the practical aspects of transfer learning.
What to transfer? This is the most important part of the whole process. We must know which part of the learning can be transferred from the source to the target in order to improve the accuracy of the target task. To answer this question, we need to identify which portion of learning is sourcespecific and which of the features are common between the source domain and target domain.
When to transfer? Sometimes, transferring knowledge for the sake of it may result in deterioration of performance (known as negative transfer). The aim of applying transfer learning should be to improve, not degrade, the performance of a target activity. We thus need to be cautious about when to transfer and when to refrain from doing so. Financial market professionals have encountered this many times in their careers, when domain experts from one market applied their understanding of those markets to other submarkets, leading to disasters. If the source and target domains are different, it would not make sense to use a bruteforce method to transfer learning in the source domain.
How to transfer? The last part of transfer learning is identifying ways in which the transfer of knowledge can be effected across domains. This requires modifications to existing algorithms or models and other adjustments.
[ 321 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
Transfer learning can be classified into three types, depending upon the relationship between the source and target domains and their respective tasks, as shown in the following table: Transfer Learning Strategies Inductive Unsupervised Transductive
Source and Target Domains Similar Different but related Different but related
Source and Target Tasks Different but related Different but related Similar
For a detailed introduction to various methods used in transfer learning, please refer to the paper A Survey on Transfer Learning by Sinno Jialin Pan and Qiang Yang, IEEE, which can be found at https://ieeexplore. ieee.org/abstract/document/5288526. In our case, labeled data is available in both target and source domains, hence we will follow the inductive transfer learning (ITL) strategy, where the network is made to learn on the source domain and is then adapted to the target domain.
Using visual inspection in transfer learning for technical analysis Our problem deals with the classification of images. There are a number of models that have been trained on the ImageNet database and that are available for use as pretrained models. These are: Xception VGG16 VGG19 ResNet50 InceptionV3 InceptionResNetV2 MobileNet DenseNet NASNet MobileNetV2
[ 322 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
We could have used any of the preceding models, but chose ResNet50 for the following reasons: The model is flexible, with the ability to skip connections— that is, adding the original input to a later layer and thus preventing information loss. Once the model is tested, it is easy to code a modified version of ResNet50 and analyze the strength of this network. We'll now use a pretrained ResNet50 model and adapt it for the purpose of analyzing charts, as a technical analyst or a trader using technical trading methods would do. The example uses simple moving average (SMA) charts, but the same model could be used for any of the chart types or technical indicators that one would look at. This brings us to the three questions that are fundamental to transfer learning, as explained in the previous section, which we need to answer in the context of our problem.
What to transfer? Charts are pictures that the human mind perceives as a whole, and not as pictures. Hence, charts are analyzed as images by the human brain. The problem at hand is one of classifying the charts to signal whether to buy a stock, sell a stock, or do nothing. This is a simple problem, and the charts we use for this purpose are also very simple, using the price series: 20day MA, and 50day MA charts, with labels for buy, sell, and none. We need to choose a deep learning model that has been used to classify images and apply it to our task since a lot of features in our charts are important, from spatial to intensity aspects of images. We chose the ResNet50 model for this purpose. Other image classification networks can be chosen, and it is a matter of preference. The ResNet50 model is deep enough for our task since the charts do not have as many complications as pictures; thus using more complicated models might increase computational requirements, without any increase in accuracy.
[ 323 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
When to transfer? Should we train a deep network or should we use an existing model? This question will be answered once we have trained using the ResNet50 network and determined the efficacy of our results. Trying to train a network from scratch would be worth the effort only if the results obtained by tweaking the ResNet50 network were not sufficiently accurate. This is a chickenandegg situation. However, given the similarity of tasks, it is worth exploring the use of pretrained models.
How to transfer? The network needs one modification to begin with. The last layer, which classifies images, needs to be dropped and reconstructed since the number of classes we need to predict are different from those predicted by ResNet50. There are many strategies we can use to transfer knowledge. We could use the network and set all the weights randomly, keeping the network structure, and determine the new weights for our problem. This would be like training a new network. We could also experiment by setting the weights as random for some of the layers and keeping the other weights the same, or we could use some sort of genetic algorithm to decide the strategy. These complications are beyond the scope of this book and are areas for future research. Here, we employ the simple strategy of using a pretrained network and using the existing weights as a starting point for our network.
Implementing a transfer learning model The basic steps in implementing a transfer learning model by adapting the ResNet50 model to our task are as follows: 1. Acquiring and formatting data: We will create charts of technical indicators, and label those charts with action labels, and save them as images. 2. Setting up data for the ResNet50 model: ResNet expects image input. Charts saved as the images that follow are set up in a directory structure compatible with the ResNet model. 3. Importing and training the model: We download the ResNet50 model, keeping all the layers and weights except the last one, which is modified specifically to our task. This model is trained with our data, and the new model is saved for use with test images.
[ 324 ]
Chart Visual Analysis by Transfer Learning
Chapter 10
Acquiring and formatting data We need to create labeled charts for analysis. The following code snippet takes the raw data from 200 randomly selected CSV files that have historical price data, and creates charts by saving them in the charts folder as images. The following steps illustrate the procedure to format data: 1. We will import the required modules to read data and define functions and label generated charts into the buy and sell categories. If the long returns are negative, then one should sell; if they are positive, then one should buy. The following functions work on the data to create labels, using forward returns as a basis: import pandas as pd import numpy as np import random import matplotlib.pyplot as plt def long_returns(df, numdays): df['buyret'] = (df.Low / df.High.shift(numdays)1)*100 df.buyret.fillna(0, inplace=True) return df def short_returns(df, numdays): df['sellret'] = (df.Low.shift(numdays) / df.High 1)*100 df.sellret.fillna(0,inplace=True) return df def label_data(df): df['Action'] = 'None' df.loc[df['buyret'] > 0.5, 'Action'] = 'Buy' df.loc[df['sellret'] > 0.5, 'Action'] = 'Sell' # df = df[df.columns.drop(['buyret','sellret'])] return df
2. The following function creates a composite chart with, 20day MA, 50day MA, and 100day MA price series: def create_labeled_charts(sample_size): chartnum = 1 i = 1 while(i iabdf', w_ij, input_layer) b_ij = tf.zeros(shape=[nb_capsules_p, nb_capsules, 1, 1], dtype=np.float32) return routing(u_hat, b_ij, nb_capsules, nb_capsules_p, iterations=iterations)
In the preceding code, for each possible parent, the capsule computes a prediction vector by multiplying its output by a weight matrix. If this output has a large scalar product with its parent, the topdown feedback is reinforced, thus increasing the coupling with this parent and reducing the coupling with other parents. Thus, the contribution this capsule makes to this parent is increased, and the contribution to other parents is reduced. This is routing by agreement. This reduces the need to segment overlapping objects in an image. The length of the output vector represented by a capsule should represent the probability that an entity represented by the capsule is present in the current input. 4. The following squashing algorithm implemented ensures that the short vectors get shrunk to 0 and long vectors are shrunk to a size just less than 1. This algorithm is nonlinear in nature: def squash(vector): vector += 0.00001 vec_squared_norm = tf.reduce_sum(tf.square(vector), 2, keep_dims=True) scalar_factor = vec_squared_norm / (1 + vec_squared_norm) /
[ 346 ]
Better Chart Analysis Using CapsNets
Chapter 11
tf.sqrt(vec_squared_norm) vec_squashed = scalar_factor * vector # elementwise return(vec_squashed)
The squashing algorithm previously defined ensures that the probability is never 1—that is, never a certainty. The preceding implementations are stored in a TensorFlow graph that will be used when training the model for our dataset of images of charts with technical indicators.
Implementing the model In Chapter 10, Chart Visual Analysis by Transfer Learning, we explored the use of saving charts of technical indicators as images and using labels about whether to buy, sell, or do nothing for these charts. The main idea behind using a pretrained image classifier was to leverage the strength of the network and detect features in the image that would not be evident from raw data as input in Chapter 6, Trading Rule Identification by CNN. However, in Chapter 10, Chart Visual Analysis by Transfer Learning, we still used a deep convolutional network that would detect the presence of a feature but ignore the spatial position of that feature in the image. We are now in a position to use the TensorFlow graph of a CapsNet using the preceding code, and train it to analyze images of charts.
Setting up data We use the dataset of charts created in Chapter 10, Chart Visual Analysis by Transfer Learning, for our classification algorithm, but, this time, rather than using only Buy and Sell categories, we use Buy, Sell, and None categories for both the training and test data. As you may recall, we had saved the charts as images. These images are copied to the subdirectory named input. The data is further split randomly into train and test subdirectories within the input directory. The following steps are followed to set up the data to be used for training our capsule network: 1. Import the necessary libraries to read images, as follows. We use the OpenCV library to read images: import import import import import
os, shutil random gc #Gabage collector for cleaning deleted data from memory cv2 numpy as np
[ 347 ]
Better Chart Analysis Using CapsNets
Chapter 11
2. We reduce the size of images to 64x64. In Chapter 10, Chart Visual Analysis by Transfer Learning, we had reduced the images to size 150x150, but since the computational complexity has increased manifold, in order to determine the effectiveness of this method in a timely fashion, it is prudent to work with a less granular version of the image. We use the same color scheme as earlier. The following code snippet shows this: nrows = 64 ncolumns = 64 channels = 3 #change to 1 if you want to use grayscale image def read_and_process_image(list_of_images): X = [] # images y = [] # labels for image in list_of_images: X.append(cv2.resize(cv2.imread(image, cv2.IMREAD_COLOR), (nrows,ncolumns), interpolation=cv2.INTER_CUBIC)) #Read the image if 'Buy' in image: y.append(1) elif 'Sell' in image: y.append(2) else: # None y.append(3) return X, y
In the preceding code: Two arrays, X and y, store the images and their labels. The X array stores images in a matrix format as returned by the OpenCV library, while the y array stores the labels that are encoded in the image name. 3. The following code splits the dataset into training, validation, and test sets for further use. The following function gets the data into arrays in the file path of training and test images: def get_data(): train_dir = 'input/train' test_dir = 'input/test' train_buys = ['input/train/{}'.format(i) for i in os.listdir(train_dir) if 'Buy' in i] #get buy images train_sells = ['input/train/{}'.format(i) for i in os.listdir(train_dir) if 'Sell' in i] #get sell images train_none = ['input/train/{}'.format(i) for i in os.listdir(train_dir) if 'None' in i] #get images to do nothing test_imgs = ['input/test/{}'.format(i) for i in os.listdir(test_dir)] #get test images
[ 348 ]
Better Chart Analysis Using CapsNets
Chapter 11
train_imgs = train_buys[:2000] + train_sells[:2000] + train_none[:2000] # slice the dataset and use 2000 in each class random.shuffle(train_imgs) # shuffle it randomly del train_buys del train_sells del train_none gc.collect()
The sample consists of an equal dataset of images with all three labels. These are then condensed into two arrays of filenames: one for the training dataset and the other for the test dataset. We then use the read_and_process_image function defined in Step 2 to read the images and resize them. The scikitlearn library is used for splitting the training validation dataset, as follows: X, y = read_and_process_image(train_imgs) del train_imgs X = np.array(X) y = np.array(y) from sklearn.model_selection import train_test_split X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.20, random_state=2) X, y = read_and_process_image(test_imgs[:100]) X_test = np.array(X) y_test = np.array(y) del X del y del test_imgs gc.collect() return X_train, y_train, X_valid, y_valid, X_test, y_test X_train, y_train, X_valid, y_valid, X_test, y_test = get_data()
The preceding code reads the names of image files in the train and test directories and stores the filenames in arrays for test images and train images. For the test images, 2,000 images are randomly selected for Buy, Sell, and None images. This is to ensure that the training sample is not biased. This data is then shuffled so as to not feed the images in an orderly pattern, thus overtraining the network for a certain set before a new set is inputted.
[ 349 ]
Better Chart Analysis Using CapsNets
Chapter 11
4. Now, set some of the parameters used for training our model, as follows: # Number of training example n_train = X_train.shape[0] # Number of validation example n_validation = X_valid.shape[0] # Number of testing example. n_test = X_test.shape[0] # What's the shape of our chart image? image_shape = X_train.shape[1:] n_classes = len(set(y_train))
In the preceding code, the number of training samples and validation samples is set using the size of input arrays created in Step 3. Thus, any changes to array sizes in Step 3 can be propagated without the need to make changes at multiple locations in code. 5. Before we proceed to train, it would be good to check the degradation of images due to downsizing. While we would like to input images that have been downsized due to ease of computing, it would be of no use if substantial information loss results from this process. We can check this by running the following code: import matplotlib.pyplot as plt import random import numpy as np import random from PIL import Image, ImageEnhance %matplotlib inline with open("signnames.csv", "r") as f: signnames = f.read() id_to_name = { int(line.split(",")[0]):line.split(",")[1] for line in signnames.split("\n")[1:] if len(line) > 0} graph_size = 3 random_index_list = [random.randint(0, X_train.shape[0]) for _ in range(graph_size * graph_size)] fig = plt.figure(figsize=(15, 15)) for i, index in enumerate(random_index_list): a=fig.add_subplot(graph_size, graph_size, i+1) imgplot = plt.imshow(X_train[index]) a.set_title('%s' % id_to_name[y_train[index]]) plt.show()
[ 350 ]
Better Chart Analysis Using CapsNets
Chapter 11
As we can see, there has been a deterioration, but most of the information has been captured. Using a more powerful machine and graphics processing unit (GPU), one could use a fullsized image, but, in the interest of training the network, these images have been downsized, as can be seen in the following diagram:
[ 351 ]
Better Chart Analysis Using CapsNets
Chapter 11
Larger image sizes increase the computational time and resource requirement. The sizing of input data is a compromise between information loss and computational complexity.
Training the model We have defined the model now and the hyperparameters are stored in a JSON file, which would be loaded at runtime. The following steps illustrate the training process. 1. We use the Keras preprocessing library for creating batches of images used for training the Capsule network. The ImageDataGenerator is an iterator that will be used by our capsule network, creating image data just in time and reducing memory overload by performing operations on the entire dataset in memory. This is crucial since the dataset is large and network computations complex. Now, let's have a look at the following code: import os os.environ['KERAS_BACKEND'] = 'tensorflow' from keras.preprocessing.image import ImageDataGenerator X_train = X_train / 255 X_valid = X_valid / 255 X_test = X_test / 255 train_datagen = ImageDataGenerator() inference_datagen = ImageDataGenerator() train_datagen.fit(X_train) inference_datagen.fit(X_valid) inference_datagen.fit(X_test)
We did not augment images since these have been downsized. Adding noise to the image might introduce information that would normally not exist. However, if fullsize images were being used, then we would use the augmented images to train as well. The model allows for this, but in the current implementation, only input images are used. 2. Now, we will define the model for our objective. Our image dataset has three different labels—Buy, Sell, and None. Thus, the final layer needs to predict as to which class an image belongs, as follows: import numpy as np from model_base import ModelBase import tensorflow as tf class ModelBuySell(ModelBase): NB_LABELS = 3 def __init__(self, model_name, output_folder):
[ 352 ]
Better Chart Analysis Using CapsNets
Chapter 11
ModelBase.__init__(self,model_name, output_folder=output_folder) def _build_inputs(self): tf_images = tf.placeholder(tf.float32, [None, 64, 64, 3], name='images') # Labels: [1,2,3] tf_labels = tf.placeholder(tf.int64, [None], name='labels') return tf_images, tf_labels
The preceding code defines the base class for our model and constructs a TensorFlow graph of input placeholders, and returns the images and corresponding labels to be used for training. 3. From the base model, we should construct the main network as shown in the following code block, to take input as preprocessed images to determine the weights of individual capsules and parent network, using the routing algorithm explained earlier: def _build_main_network(self, images, conv_2_dropout): shape = (self.h.conv_1_size, self.h.conv_1_size, 3, self.h.conv_1_nb) conv1 = self._create_conv(self.tf_images, shape, relu=True, max_pooling=False, padding='VALID') shape = (self.h.conv_2_size, self.h.conv_2_size, self.h.conv_1_nb, self.h.conv_2_nb) conv2 = self._create_conv(conv1, shape, relu=True, max_pooling=False, padding='VALID') conv1 = tf.nn.dropout(conv1, keep_prob=conv_2_dropout) caps1 = conv_caps_layer(input_layer=conv1, capsules_size=self.h.caps_1_vec_len,nb_filters=self.h.caps_1_nb_fil ter,kernel=self.h.caps_1_size) caps2 = fully_connected_caps_layer(input_layer=caps1, capsules_size=self.h.caps_2_vec_len, nb_capsules=self.NB_LABELS, iterations=self.h.routing_steps) return caps1, caps2
This method is used to create two convolutions and a CapsNet on top. The preceding output from the capsules layer returned needs to be propagated to the parent nodes. 4. We require a decoder for the output of a second capsule layer to propagate to the parent nodes and then predict the class, as follows: def _build_decoder(self, caps2, one_hot_labels, batch_size): labels = tf.reshape(one_hot_labels, (1, self.NB_LABELS, 1)) mask = tf.matmul(tf.squeeze(caps2), labels, transpose_a=True)
[ 353 ]
Better Chart Analysis Using CapsNets
Chapter 11
capsule_vector = tf.reshape(mask, shape=(batch_size, self.h.caps_2_vec_len)) # Reconstruct image fc1 = tf.contrib.layers.fully_connected(capsule_vector, num_outputs=400) fc1 = tf.reshape(fc1, shape=(batch_size, 5, 5, 16)) upsample1 = tf.image.resize_nearest_neighbor(fc1, (8, 8)) conv1 = tf.layers.conv2d(upsample1, 4, (3,3), padding='same', activation=tf.nn.relu) upsample2 = tf.image.resize_nearest_neighbor(conv1, (16, 16)) conv2 = tf.layers.conv2d(upsample2, 8, (3,3), padding='same', activation=tf.nn.relu) upsample3 = tf.image.resize_nearest_neighbor(conv2, (32, 32)) conv3 = tf.layers.conv2d(upsample3, 16, (3,3), padding='same', activation=tf.nn.relu) upsample4 = tf.image.resize_nearest_neighbor(conv2, (64, 64)) conv6 = tf.layers.conv2d(upsample4, 16, (3,3), padding='same', activation=tf.nn.relu) # 3 channel for RGG logits = tf.layers.conv2d(conv6, 3, (3,3), padding='same', activation=None) decoded = tf.nn.sigmoid(logits, name='decoded') tf.summary.image('reconstruction_img', decoded) return decoded
The vector output of the second layer of capsules gets information from the child capsule layer from capsule layer 1. The features detected by capsule layer 1 are propagated to the parent capsules and are then decoded to output the prediction. 5. We will initialize a graph to record our progress on TensorBoard, as follows: def init(self): self.tf_images, self.tf_labels = self._build_inputs() self.tf_conv_2_dropout = tf.placeholder(tf.float32, shape=(), name='conv_2_dropout') batch_size = tf.shape(self.tf_images)[0] one_hot_labels = tf.one_hot(self.tf_labels, depth=self.NB_LABELS) self.tf_caps1, self.tf_caps2 = self._build_main_network(self.tf_images, self.tf_conv_2_dropout)
The preceding code sets up the graph to define the main network for identification of salient characteristics of the image and mapping those to the labels.
[ 354 ]
Better Chart Analysis Using CapsNets
Chapter 11
6. Once the network can map the characteristics of images to labels, these characteristics need to be decoded, as in the preceding Step 4, to predict the class, as follows: self.tf_decoded = self._build_decoder(self.tf_caps2, one_hot_labels, batch_size)
From the preceding code, we have the predictions, and we can compare those with the labels for images to determine the prediction accuracy. 7. The following code is a placeholder for the loss function— that is, the level of inaccuracy in prediction (the build loss function is defined later, in Step 10): _loss = self._build_loss( self.tf_caps2, one_hot_labels, self.tf_labels, self.tf_decoded, self.tf_images) (self.tf_loss_squared_rec, self.tf_margin_loss_sum, self.tf_predicted_class, self.tf_correct_prediction, self.tf_accuracy, self.tf_loss, self.tf_margin_loss, self.tf_reconstruction_loss) = _loss
The aim of prediction is to minimize the loss, as defined by the preceding loss function. 8. The next step of optimization using the Adam optimizer does just that: it minimizes the loss function defined in Step 7. This is the placeholder for optimization, and the trainable flag will be set to false when training commences, as shown in the following code block: # Build optimizer optimizer = tf.train.AdamOptimizer(learning_rate=self.h.learning_rate) self.tf_optimizer = optimizer.minimize(self.tf_loss, global_step=tf.Variable(0, trainable=False))
The computational requirements for the network are immense. The weights of every capsule are estimated and are then propagated to parent nodes, and, finally, predicted using a decoder, resulting in the learning process being very slow. It is useful to see progress on the TensorBoard to determine early termination. This is helpful in saving time if the progress is not visible and the network has hit a learning plateau.
[ 355 ]
Better Chart Analysis Using CapsNets
Chapter 11
9. As shown in the following code, the values logged for loss and accuracy levels of the network can be analyzed using graphics to demonstrate the learning progress and identify any signs of local optima. This would be helpful for hyperparameter tuning: # Log value into tensorboard tf.summary.scalar('margin_loss', self.tf_margin_loss) tf.summary.scalar('accuracy', self.tf_accuracy) tf.summary.scalar('total_loss', self.tf_loss) tf.summary.scalar('reconstruction_loss', self.tf_reconstruction_loss) self.tf_test = tf.random_uniform([2], minval=0, maxval=None, dtype=tf.float32, seed=None, name="tf_test") self.init_session()
The preceding code logs the value on TensorBoard for visualisation of loss minimization as training progresses. 10. The following code defines the loss function, to be used during the optimization for loss minimization: def _build_loss(self, caps2, one_hot_labels, labels, decoded, images): capsules_length = tf.sqrt(tf.reduce_sum(tf.square(caps2), axis=2, keep_dims=True)) max_l = tf.square(tf.maximum(0., 0.9  capsules_length)) max_l = tf.reshape(max_l, shape=(1, self.NB_LABELS)) max_r = tf.square(tf.maximum(0., capsules_length  0.1)) max_r = tf.reshape(max_r, shape=(1, self.NB_LABELS)) t_c = one_hot_labels m_loss = t_c * max_l + 0.5 * (1  t_c) * max_r margin_loss_sum = tf.reduce_sum(m_loss, axis=1) margin_loss = tf.reduce_mean(margin_loss_sum)
The length of the instantiation vector denotes whether a capsule is present in the image. Margin loss is computed for each of the capsules in the preceding code, to account for the presence of multiple capsules in an image. 11. A separate reconstruction loss is computed that measures the difference between the output of logistic units and pixel densities, as shown in the following code block: loss_squared_rec = tf.square(decoded  images) reconstruction_loss = tf.reduce_mean(loss_squared_rec) loss = margin_loss + (0.0005 * reconstruction_loss)
[ 356 ]
Better Chart Analysis Using CapsNets
Chapter 11
predicted_class = tf.argmax(capsules_length, axis=1) predicted_class = tf.reshape(predicted_class, [tf.shape(capsules_length)[0]]) correct_prediction = tf.equal(predicted_class, labels) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) return (loss_squared_rec, margin_loss_sum, predicted_class, correct_prediction, accuracy, loss, margin_loss, reconstruction_loss)
The preceding code returns the losses and prediction accuracy. 12. The network is then trained with an image dataset by minimizing the difference between the input image and the reconstructed image. The weights are estimated using Adam optimizer, as shown in the following code block: def optimize(self, images, labels, tb_save=True): tensors = [self.tf_optimizer, self.tf_margin_loss, self.tf_accuracy, self.tf_tensorboard] _, loss, acc, summary = self.sess.run(tensors, feed_dict={ self.tf_images: images, self.tf_labels: labels, self.tf_conv_2_dropout: self.h.conv_2_dropout }) if tb_save: # Write data to tensorboard self.train_writer.add_summary(summary, self.train_writer_it) self.train_writer_it += 1 return loss, acc
The preceding code is the optimization function fed into the Adam optimizer as defined in Step 8. 13. During the training process, it is a good idea (as explained in Chapter 3, Construction, Testing and Validation of Models) to be validating the network weights. It can give indications of overfitting, which—if present—requires corrective action and reduces the reliability of the results. To do this, run the following code: def evaluate(self, images, labels, tb_train_save=False, tb_test_save=False): tensors = [self.tf_margin_loss, self.tf_accuracy, self.tf_tensorboard] loss, acc, summary = self.sess.run(tensors, feed_dict={ self.tf_images: images,
[ 357 ]
Better Chart Analysis Using CapsNets
Chapter 11
self.tf_labels: labels, self.tf_conv_2_dropout: 1. }) if tb_test_save: # Write data to tensorboard self.test_writer.add_summary(summary, self.test_writer_it) self.test_writer_it += 1 if tb_train_save: # Write data to tensorboard self.train_writer.add_summary(summary, self.train_writer_it) self.train_writer_it += 1 return loss, acc
The preceding code defines the function where images are fed to the estimated network and accuracy and loss recorded for every batch. When logged on TensorBoard, one can visualize the improvement, plateau, or deterioration in the training process. 14. The following code uses a fully trained model, saved as a checkpoint, to predict the labels of test images to check for accuracy on an outofsample dataset: def predict(self, images): tensors = [self.tf_caps2] caps2 = self.sess.run(tensors, feed_dict={ self.tf_images: images, self.tf_conv_2_dropout: 1. })[0] caps2 = np.sqrt(np.sum(np.square(caps2), axis=2, keepdims=True)) caps2 = np.reshape(caps2, (len(images), self.NB_LABELS)) softmax = np.exp(caps2) / np.sum(np.exp(caps2), axis=1, keepdims=True) return softmax def reconstruction(self, images, labels): tensors = [self.tf_decoded] decoded = self.sess.run(tensors, feed_dict={ self.tf_images: images, self.tf_labels: labels, self.tf_conv_2_dropout: 1. })[0] return decoded
The preceding code is used to make outofsample predictions using test data created in Step 3 of the Setting up data section.
[ 358 ]
Better Chart Analysis Using CapsNets
Chapter 11
15. Since our dataset cannot fit inside the GPU, we need to evaluate the entire data for every batch, and that is accomplished with the following code: def evaluate_dataset(self, images, labels, batch_size=10): tensors = [self.tf_loss_squared_rec, self.tf_margin_loss_sum, self.tf_correct_prediction, self.tf_predicted_class] loss_squared_rec_list, margin_loss_sum_list = None, None correct_prediction_list, predicted_class = None b = 0 for batch in self.get_batches([images, labels], batch_size, shuffle=False): images_batch, labels_batch = batch loss_squared_rec, margin_loss_sum, correct_prediction, classes = self.sess.run(tensors,feed_dict={self.tf_images: images_batch, self.tf_labels: labels_batch, self.tf_conv_2_dropout: 1.}) if loss_squared_rec_list is not None: predicted_class = np.concatenate((predicted_class, classes)) loss_squared_rec_list = np.concatenate((loss_squared_rec_list, loss_squared_rec)) margin_loss_sum_list = np.concatenate((margin_loss_sum_list, margin_loss_sum)) correct_prediction_list = np.concatenate((correct_prediction_list, correct_prediction)) else: predicted_class = classes loss_squared_rec_list = loss_squared_rec margin_loss_sum_list = margin_loss_sum correct_prediction_list = correct_prediction b += batch_size margin_loss = np.mean(margin_loss_sum_list) reconstruction_loss = np.mean(loss_squared_rec_list) accuracy = np.mean(correct_prediction_list) loss = margin_loss return loss, accuracy, predicted_class
The preceding code has defined the TensorFlow graph of our CapsNet model, keeping in mind the nuances of our problem.
[ 359 ]
Better Chart Analysis Using CapsNets
Chapter 11
16. We are now ready to initialize and train the model in batches. This is done using the following code. We train in batch sizes of 50, due to heavy computational requirements. This is akin to the code in Chapter 3, Construction, Testing and Validation of Models, which has been explained in detail previously: model = ModelBuySell("BuySell", output_folder="outputs") model.init() BATCH_SIZE = 50 def plot_progression(b, cost, acc, label): print( "[%s] Batch ID = %s, loss = %s, acc = %s" % (label, b, cost, acc))
The preceding code initializes the model and defines the function to plot the progression batch by batch, for the data scientist to monitor progress. 17. The following code is where the training of our model takes place: b = 0 valid_batch = inference_datagen.flow(X_valid, y_valid, batch_size=BATCH_SIZE) best_validation_loss = None augmented_factor = 0 decrease_factor = 0.5 train_batches = train_datagen.flow(X_train, y_train, batch_size=BATCH_SIZE) #augmented_train_batches = train_datagen_augmented.flow(X_train, y_train, batch_size=BATCH_SIZE) while True: x_batch, y_batch = next(train_batches) ### Training cost, acc = model.optimize(x_batch, y_batch) ### Validation x_batch, y_batch = next(valid_batch, None) # Retrieve the cost and acc on this validation batch and save it in tensorboard cost_val, acc_val = model.evaluate(x_batch, y_batch, tb_test_save=True)
In this model, we have the set the augmented factor of the image to 0. This is a placeholder so as to require little modification if image augmentation is experimented with.
[ 360 ]
Better Chart Analysis Using CapsNets
Chapter 11
18. For every 10 batches, the results are plotted, and after every 1,000 batches, an evaluation is made on the entire dataset to avoid losing infrequent information. This is implemented in the following code: if b % 10 == 0: # Plot the last results plot_progression(b, cost, acc, "Train") plot_progression(b, cost_val, acc_val, "Validation") if b % 1000 == 0: # Test the model on all the validation print("Evaluate full validation dataset ...") loss, acc, _ = model.evaluate_dataset(X_valid, y_valid) print("Current loss: %s Best loss: %s" % (loss, best_validation_loss)) plot_progression(b, loss, acc, "TOTAL Validation") if best_validation_loss is None or loss < best_validation_loss: best_validation_loss = loss model.save() augmented_factor = augmented_factor * decrease_factor print("Augmented Factor = %s" % augmented_factor) b += 1
The training progress is visible on TensorBoard for training data and validation data, with validations performed after every 1,000 epochs. It takes a long time (over 10 hours on a machine without a GPU) for the model to be fully trained. After a manual intervention, the results are as follows: Training batch id : 1100 Loss : 0.0489 Validation batch id : 1100 Loss: 0.148
Accuracy : 0.7 Accuracy : 0.54
These are very good results for a model trained for a few epochs only, and with data considerably downsized. 19. The power of generalization from learning capsules comes to the fore when predicting test images, as shown in the following code: loss, acc, predicted_class = model.evaluate_dataset(X_test, y_test)
On the test dataset, the accuracy of the results is 93%. When results are this good, it is time to be skeptical. We have made a lot of assumptions in the preceding model, from image downsizing to the number of classes being predicted. The time taken to estimate a model for a threeclass prediction is too high to make the network of any practical use for multiclass predictions, and if we were to incorporate more information.
[ 361 ]
Better Chart Analysis Using CapsNets
Chapter 11
Summary In this chapter, we have learned how to capture the information about the spatial presence of features in an image using a relatively new class of deep learning models—CapsNets. The concept is simple. A CapsNet consists of capsules that are mini networks, focusing on only parts and features of an image. These are then aggregated into higherlevel capsules that capture larger parts of the image and preserve information about the presence and location of individual features detected by capsules. This enables the network to differentiate two images that might have the same number of features but different locational characteristics. CapsNets thus offer a tremendous improvement over convolutional networks for image classification. From our sample data, the results are very encouraging, but training the model is a very intensive compute task. CapsNets definitely require a heavy GPU training environment; possibly, using a tensor processing unit (TPU) might be faster for our TensorFlow implementation. From the prediction accuracy, it is heartening that the model does live up to its reputation of being able to generalize the features and hence correctly classify images where information is lost after this downsizing. It could, therefore, be a good substitute for human chartists, since it is able to capture the dynamics of a chart image, as a chartist would. It is difficult to draw conclusions that this should be the best method given the costs involved, but it is definitely worth keeping an eye out about developments in this space. We should take heart that all that we have learned till now can come into action when training bots for automated trading. That is the focus of the next chapter.
[ 362 ]
12 Training Trader Robots Using Deep Reinforcement Learning Algorithmic trading has gained enormous popularity in the last few decades. The majority of trades made in US stocks are automatically generated by algorithms. "Eighty percent of the daily volume in the US is done by machines." De Blonay, fund manager at Jupiter Asset Management, in December 2018
– Guy
Such algorithms are based on technical indicators, advanced statistics, or any mathematical model. In recent years, DL methods have also been largely adopted thanks to their ability to detect and exploit complex patterns in financial data. On the other hand, one of the most promising developments in AI is reinforcement learning (RL). The aim of RL is not to model the market, although, in many applications, modeling is an intermediate step of RL. The main goal of RL is to choose a sequence of actions so as to maximize the longterm reward. For this reason, RL seems to be well suited for learning trading rules. In addition, RL can be combined with DL in order to learn complex features from data. This approach has been proved to be very powerful in difficult tasks, such as playing games. In this chapter, we will face the problem of training a robot trader by backtesting its behavior using rules based on wellknown technical indicators of historical data. These trading rules will be learned by using RL. In particular, we will focus on deep Qlearning, a technique that was popularized by DeepMind and based on neural networks. This chapter will also discuss issues concerning stability and convergence and the solutions for solving them.
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
This chapter will cover the following topics: Understanding reinforcement learning and Deep Qlearning Formulating the RL problem Configuring the data Implementing a robot based on Deep Qlearning
Understanding Reinforcement Learning RL is an area of ML that allows us to deal with sequential decision problems. It is based on an agent that learns from the environment by interacting with it and receiving rewards for performing actions. The elements of RL are as follows: Agent: This is the learning component that makes a decision regarding what action to take in order to maximize the reward. Environment: This is the physical world that the agent interacts with, and it changes as the agent performs actions. Policy: Defines the agent's behavior at a given time by mapping from states to actions. Reward function: Defines the goal in an RL problem and maps each state/action pair to a numerical reward. Value function: Represents the cumulative future reward in the long run. Model of the environment: Predicts the behavior of the environment. This is optional. The following diagram shows the general flow of RL:
[ 364 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
The steps for RL, as presented in the preceding diagram, are as follows: 1. The agent observes a state from the environment. 2. The agent executes an action based on the policy. 3. The agent receives the corresponding reward from the environment, together with the next state. 4. Information about the reward for that state/action pair is recorded and used to update the policy. This is repeated until the environment sends the last state, finishing the episode. Therefore, an episode is a sequence of states, actions, and rewards, which ends with a terminal state. The goal of the agent is to learn which succession of actions maximizes the cumulative future reward. The reward is defined by a human and it is critical for the model's convergence and its success. Among the available RL algorithms, Qlearning is a widely used approach that does not require a model of the environment (it is a modelfree algorithm). Its goal is to learn a policy to model the agent's action selection through trial and error. The agent learns the optimal policy using an absolute greedy strategy and behaves using other policies, such as the greedy policy (which is an offpolicy algorithm). This will be discussed in detail later. Let's dive into the mathematics of the algorithm. At each time step , the agent observes the state of the environment and takes an action , then receives some reward and a new state . Qlearning makes use of the value function , which approximates the future reward based on the (state, action) pair. It is defined as follows:
Here, refers to the expectation, denotes the decision policy, and discounted reward. This is given by the following equation:
is the cumulative
Here, is the discount factor to weight the future reward. The goal is to learn an optimal policy so as to maximize the expected return, which can be defined as follows:
[ 365 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
According to the Bellman equation, we can rewrite the previous equation as a recursive definition of the value function:
This means that the maximum discounted reward for an action in the state is equal to the immediate reward, plus the maximum possible reward from the future action in the incoming state . Therefore, the convergence to the optimal Q function relies on a value iteration procedure.
Deep Qlearning The main drawback of the Qlearning algorithm is its lack of generality, which means it does not perform well when the number of states and/or actions is large. Deep Qlearning solves this issue by using a deep neural network (DNN) to approximate the Qvalues for each action, based on a given state. This allows the agent to take the action that maximizes the Qvalue. The socalled Qnetwork is able to extract the main features from the states that the agent has been in before in order to predict the reward even for unseen states. This approach of combining RL and DNN to approximate the Qvalue is known as Deep Qlearning, and it was proposed in 2013 by DeepMind to play Atari games. The following diagram represents the generalized form of the Deep Qlearning algorithm:
The Qnetwork receives the current state as input and gives the corresponding Qvalue for each action as output. It is trained based on the Qlearning update equation. The loss function can be written as follows:
[ 366 ]
Training Trader Robots Using Deep Reinforcement Learning
Here, indicates the current training epoch and
Chapter 12
represents the network parameters.
Deep Qlearning is known to be unstable due to the nonlinearities concerning the use of a neural network to approximate the Qvalue. This instability is mainly caused by the high correlation between contiguous states, making the Qnetwork biased and hindering its convergence. The technique that's used to solve this issue during training is known as the experience replay. It consists of storing all the experiences, including state transitions, actions, and rewards, and then randomly taking a minibatch of samples from the memory to update the network. In the next section, we will implement a robot trader based on the DeepQ learning algorithm in order to identify the relationship between market indicators and decisions taken by some (unknown) strategy.
Formulating the RL problem When formulating an RL problem, we need to define three elements: the observation of the environment or state, the possible actions taken by the agent, and a reward system.
State For algorithmic trading, the definition of the environment is based on the financial market. The environment features can consist of historical raw data, returns, technical indicators, and more. In our example, the state at a given time step t will be represented as a NumPy array of a shape of [None, look_back, n_features], where look_back is a hyperparameter indicating the number of time steps considered before t and n_features is the number of technical indicators in the input dataset, which we will specify later on. We impose a look back because, at each time t, the strategy may depend on the previous time steps.
Action The number of actions will be the same as there are market positions. In our case, the agent can perform three actions, which are coded as follows: +1 for long positions, which means that a hypothetical investor buys a stock that is expected to increase in value.
[ 367 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
−1 for short positions, meaning that our hypothetical investor sells a stock that they don't actually own, believing that its value is going to decrease, and then buying it back at a lower price. 0 for hold positions, which means that the investor neither buys nor sells the stock, so there is no change in the number of shares they have. The agent must learn to decide on the sequence of actions that maximize the cumulative reward.
Reward There are many different ways to define the reward system, and this choice will allow the model to converge or not. For this chapter's experiment, we can think of two ways to define the reward system: +1 if the action predicted by the agent is the same as the position assumed by the strategy; otherwise, the reward will be 1. +1 if the predicted action matches the position taken by the strategy, 0 if the predicted action is hold and the actual position is long or short, and 1 otherwise. Either of these systems is valid, but the second set of rewards penalizes when the agent incorrectly predicts long or short positions since this situation is less desirable. In fact, if the hold position is predicted in the wrong way, we wouldn't lose any money from our decision. For this reason, we will implement the second system of rewards. Every RL problem makes use of different reward systems and, sometimes, the rewards may have different magnitudes. For instance, in the Atari game Asteroids, the agent can receive between 20 and 1,000 points in a single step. This difference in the reward magnitudes may lead to instability during training. It is good practice to clip the rewards so that they lie in the [1,1] interval in order to make the model more robust. Now that we have formulated the RL problem, we can configure the data that's going to be used by the model.
Configuring the data This section involves obtaining and preparing the dataset so that it can be used by the model.
[ 368 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Loading the data For the experiment in this chapter, we will use historical data concerning the adjusted close price series of the American Airlines Group Inc. (AAL) stock. The data range is the same as the one we used in Chapter 4, Index Replication Using Autoencoders, which is from January 1, 2013, to December 31, 2018. Thus, we can load the historical data regarding the NASDAQ100 stocks that we gathered and saved in that chapter. To do this, we'll use the Pandas library: # Load NASDAQ100 data alldata = pd.read_pickle('nasdaq100_6y.pkl') # Load Symbol to NumPy array symbol = 'AAL' data = alldata[symbol].values
The NumPy data array will contain the close price series of the AAL stock during the dates we mentioned previously.
Defining a trading strategy Now that we have loaded the raw price series, it's time to define the trading strategy that will generate the market positions and the technical indicators that we will use as input data. The strategy that we've chosen for our example is based on the Moving Average Convergence Divergence (MACD) histogram. The MACD histogram is calculated as the difference between the MACD line and its signal line. The signal line is the nine period exponential MA (EMA) of the MACD line, which is, in turn, a faster 12period EMA minus a slower 26period EMA of the stock's closing price. Therefore, the resulting MACD histogram is a momentum indicator that oscillates above and below 0. We can consider the MACD histogram a momentum indicator that allows us to generate entry/exit signals, as follows: Buy Entry: Histogram value gets smaller than −0.4 Buy Exit: Histogram overcomes the value −0.1 Sell Entry: Histogram value is above +0.4 Sell Exit: Histogram goes below the value +0.1
[ 369 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
To compute the MACD histogram and other technical indicators, we will use TALib, which is a library that's widely diffused among traders for the technical analysis of financial data. Before using TALib for Python, you will need to install TALib from http://talib. org/hdr_dw.html. Then, you can install the Python wrapper directly from PyPI (pip install TALib). The following code calculates the MACD line, the signal line, and the MACD histogram using the MACD function from TALib: import talib macd, macdsignal, macdhist = talib.MACD(close, fastperiod=12, \ slowperiod=26, signalperiod=9)
Let's cancel the Not a Number (NaN) values at the beginning of these arrays, which are due to the moving averages: # Cancel NaN values macdhist = macdhist[~np.isnan(macdhist)] macd = macd[len(macdhist):] macdsignal = macdsignal[len(macdhist):]
The value of the MACD histogram inevitably depends on the stock price variations. Since we are interested in defining a strategy that is independent of stock prices, we need to normalize the histogram values to the [−1,+1] range. This can be done by using the MinMaxScaler() object from the scikitlearn library: from sklearn import preprocessing # Scaling MACD hist to [0, 1] range min_max_scaler = preprocessing.MinMaxScaler(feature_range=(1, 1)) macdhist_norm = min_max_scaler.fit_transform(np.expand_dims(macdhist, axis=1))
Now, we are ready to implement the strategy. Let's get started: 1. First, we set the threshold for the entry/exit position, as follows: # Set thresholds start_sell = 0.4 stop_sell = 0.1 start_buy = 0.4 stop_buy = 0.1
[ 370 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
2. Then, we create a Numpy array whose length is the same as the histogram and filled with NaN values. To do this, we use the .full routine from the NumPy library: # Create array of positions y = np.full(len(macdhist), np.nan)
3. After that, we set the initial value of the array to 0: # Set initial position y[0] = 0
4. Finally, we implement the strategy according to the preceding conditions using ifelse statements: # Implement strategy for i in range(1, len(macdhist)): if y[i1] == 0: if macdhist_norm[i] >= start_sell: # Enter sell position y[i] = 1 elif macdhist_norm[i] stop_sell: # Stay in sell position y[i] = 1 else: # Leave sell position y[i] = 0 else: if macdhist_norm[i] < stop_buy: # Stay in buy position y[i] = 1 else: # Leave buy position y[i] = 0
[ 371 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Let's visualize the trading decisions assumed by this strategy over a limited period of time. We can use the matplotlib library to do this, as follows: import matplotlib.pyplot as plt # Plot strategy dates = np.arange(len(macdhist)) plt.plot(dates, y,'g', label='Strategy Positions') plt.bar(dates, macdhist_norm[:, 0], width=1, color='blue', label='MACD histogram') plt.plot(dates, start_sell * np.ones(len(macdhist)), 'k', lw=1) plt.plot(dates, stop_sell * np.ones(len(macdhist)), 'k', lw=1) plt.plot(dates, start_buy * np.ones(len(macdhist)), 'k', lw=1) plt.plot(dates, stop_buy * np.ones(len(macdhist)), 'k', lw=1) plt.xlabel('Days') plt.xlim((300, 600)) plt.legend() plt.show()
The following graph displays the MACD histogram, along with the strategy positions for 300 days. We can also see the thresholds:
The preceding graph helps us understand the behavior of the MACD strategy we defined previously.
[ 372 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Input data In this chapter's experiment, we'll complicate the task at hand by requiring the model to combine multiple series in order to get the trading signal. We will use different technical indicators as input features, including the MACD histogram, which provides the trading signal, the MACD line, and the signal line, which are correlated to the decisions, and some additional indicators that are not related to the trading signal, as summarized in the following table: Indicator description Midpoint over period Hilbert transformDominant cycle period Highest value over a specified period (MAX) Vector trigonometric sine (SIN) Absolute price oscillator (APO)
Type of indicator Overlap studies Cycle indicator Math operator Math operator Momentum
All the technical indicators that we identified in the preceding table are available in the TALib library: ind1 = talib.MIDPOINT(close) ind2 = talib.HT_DCPERIOD(close) ind3 = talib.MAX(close) ind4 = talib.SIN(close) ind5 = talib.APO(close)
# MidPoint over period # Hilbert Transform (Dominant Cycle Period) # Highest value over a specified period # Vector Trigonometric Sin # Absolute Price Oscillator
Finally, we concatenate all the input features into one unique NumPy array using the vstack() function: X = np.vstack((macdhist, macd, macdsignal, ind1[len(macdhist):], \\ ind2[len(macdhist):], ind3[len(macdhist):], ind4[len(macdhist):], \\ ind5[len(macdhist):])) X = X.T
Let's inspect the shape of the resulting NumPy array, X: print(x.shape)
Here's the output: (1477, 8)
Therefore, the input dataset consists of eight series of 1,477 time steps.
[ 373 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Data preparation The dataset that will be used for training and evaluating the RL model consists of the set of technical indicators mentioned previously. On the other hand, the trading signal that's generated by the strategy will be used for defining the reward, as we will see later. Just like we did for the deep learning models we implemented in the previous chapters, we need to split the dataset into train and test sets to train the agent and test its performance. The following code splits the data into an 80/20 split: # Split dataset n_train = int(X.shape[0] * 0.8) X_train, y_train = X[:n_train], y[:n_train] X_test, y_test = X[n_train:], y[n_train:]
The input data needs to be scaled to the [0,1] interval since it will be given as input to the Qnetwork. We will resort to the MinMaxScaler() object to do this: # Normalize data scaler = preprocessing.MinMaxScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)
Finally, we need to encode the trading signal in order to compare it with the actions taken by the agent. In the following code snippet, we're using the LabelEncoder() object from scikitlearn to encode the decisions that are undertaken by the strategy regarding entering or quitting a position: # Encode trading signal with integers between 0 and n1 classes le = preprocessing.LabelEncoder() y_train = le.fit_transform(y_train) y_test = le.transform(y_test)
Let’s have a look at the array of positions that were identified by the le object: print(le.classes_)
The output is as follows: [1.
0. 1.]
[ 374 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
As we can see, the short position (1) corresponds to element 0 in the array, the hold position (0) is element 1 in the array, and the long position (+1) is element 2 in the array. Now, let's turn to the RL model itself. In the next section, we will implement the DeepQ learning model with the aim of identifying the trading rules that have been applied to the market by the MACD strategy we defined in the previous section. It is worth noting that these rules are not known to the agent, which receives the reward in delayed time.
Implementing a Robot based on Deep Qlearning In this section, we will illustrate how to implement the deep Qlearning model, including the design of the agent, how to train it in order to maximize the final cumulated reward, and evaluating the model in terms of accuracy.
Designing the agent We will design a basic agent based on the Deep Qlearning implementation at https:// keon.io/deepqlearning/ so that it learns how to play CartPole. First of all, we will import all the libraries that will be used by the agent. This includes NumPy, for working with multidimensional data, random, for generating random numbers, collections.deque, for saving the previous observations of the environment, and some Keras classes for implementing the DQN: import numpy as np import random from collections import deque from keras.models import Sequential from keras.layers import Dense, Flatten
[ 375 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
The following code creates a class named Agent and defines the __init__() function, which will be called every time we create a new object. This function assigns values for look_back, action_size, and n_features. These are passed as arguments. look_back is the size of the window that we consider at each time step, action_size is the number of possible actions, and n_features is the number of input features to the model. The __init__() function also creates a list called memory to save the previous experiences and assigns values to the hyperparameters that are used during training. In addition, it creates the DQN by calling the create_DQN() method: # Deep Qlearning Agent class Agent: def __init__(self, look_back, action_size, n_features): self.look_back = look_back # fixed window of historical prices self.action_size = action_size # buy, sell, hold self.n_features = n_features # input space self.memory = deque(maxlen=3000) # list of experiences self.gamma = 0.95 # discount factor self.epsilon = 1.0 # exploration rate self.epsilon_min = 0.01 self.epsilon_decay = 0.995 self.DQN = self.create_DQN() # DQN
In the following subsections, we will explain and implement all the methods that are contained in the Agent class.
DQN The method that creates the DQN using Keras is named create_DQN(). It creates a multilayer perceptron (MLP) that takes twodimensional data the shape of [look_back, n_features] and gives onedimensional data representing the approximations of the different Qvalues for each action as output. The MLP consists of three Dense hidden layers containing 30, 15, and 5 neurons, respectively, and uses all of them while using the ReLU as the activation function. Then, we add a Flatten layer in order to reduce the number of dimensions to 1. The output layer is a Dense layer with as many neurons as the number of actions being considered and uses the linear activation function. Finally, we compile the model using the Adam optimizer, the MSE as the loss function, and the accuracy as the evaluation metric: def create_DQN(self): model = Sequential() model.add(Dense(30, input_shape=(self.look_back, self.n_features),
[ 376 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
activation='relu')) model.add(Dense(15, activation='relu')) model.add(Dense(5, activation='relu')) model.add(Flatten()) model.add(Dense(self.action_size, activation='linear')) model.compile(optimizer='adam', loss='mse', metrics=['accuracy']) return model
The preceding code implements the create_DQN() method to create and compile the deep Qnetwork that's used by the agent to approximate the Qvalues at each time step during training.
Remember Previously, we created an empty list that we will use to keep the memory of the last 3,000 experiences so that the agent does not forget any of them when training the model over time. The memory is a list that includes the current state, the action taken by the agent, the resulting reward, and the next state received from the environment. It also includes a boolean called done that indicates if the next state is the final one. The following remember() function is aimed at storing states, actions, and rewards by appending elements to the memory list: def remember(self, state, action, reward, next_state, done): self.memory.append((state, action, reward, next_state, done))
The preceding function will be called after each time step during the training stage.
Experience replay The replay() method trains the DQN according to the experience replay strategy. The necessary steps are as follows: 1. First, we randomly choose a small group of experiences that were previously stored in memory. We call this the minibatch. We can use the .sample() method from the random library to do this. Note that the size of the minibatch (batch_size) is passed as an argument to the replay() method.
[ 377 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
2. For each experience in the minibatch, we do the following: We estimate the future discounted reward according to the Bellman equation we described previously. We use the NumPy .amax() function to take the maximum predicted future reward. We calculate the Qvalues associated with the current state and then we update them according to the Bellman equation. In this way, we take into account both the immediate rewards and the future possible rewards in order to maximize the cumulative future reward. 3. Then, we fit the DQN for one epoch with the current state as the input data and the updated Qvalues as the target data. 4. Finally, we decrease the epsilon value up to a certain limit (epsilon_min) while the agent is gathering experiences: def replay(self, batch_size): # Random minibatch of experiences mini_batch = random.sample(self.memory, batch_size) # Information from each experience for state, action, reward, next_state, done in mini_batch: if done: # End of episode, make our target reward target = reward else: # estimate the future discounted reward target = reward + self.gamma * np.amax(self.model.predict(next_state)[0]) # Calculate the target associated to the current state target_f = self.model.predict(state) # Update the Qvalue for the action according # to Belmann equation target_f[0][action] = target # Train the MLP with the state and target_t self.model.fit(state, target_f, epochs=1, verbose=0) # Update epsilon if self.epsilon > self.epsilon_min: self.epsilon *= self.epsilon_decay
The preceding code implements the replay method aimed at training the Deep QNetwork (DQN).
[ 378 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Act The act() method decides which action to take according to the epsilongreedy policy. In such a policy, we either take an action at random with a probability of epsilon or we choose the action that maximizes the future reward with a probability of 1epsilon. The following code implements the act() function: def act(self, state): if np.random.rand() batch_size: agent.replay(batch_size)
8. In addition, the run() function saves the model weights and the epsilon variable every five episodes in order to use them later if necessary. The list of total rewards is also saved: if (ep + 1) % 5 == 0 and ep > 0: file = 'AAL_robot_checkpoint' + str(ep + 1) # Serialize weights to HDF5 agent.model.save_weights(file + ".h5") # Save epsilon pickle.dump(agent.epsilon, open(file + "_epsilon.pickle", "wb")) # Save list of rewards along the epochs np.savetxt(file + '_total_reward.txt', total_reward_list) return
[ 381 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
9. Now, we are ready to train the agent by calling the run() function from the main script, as follows: # Train Agent run(agent, X_train, y_train, episodes, look_back)
10. Let's visualize the evolution of the total reward over the episodes: # Load rewards total_reward_list = np.loadtxt('AAL_robot_checkpoint' + \ str(episodes) + ' _total_reward.txt') # Plot plt.figure() plt.plot(np.arange(1, episodes+1), total_reward_list) plt.xlabel('Episodes') plt.ylabel('Total Reward') plt.show()
The output of the preceding code is the following graph:
As we can see, the total reward increases significantly along with the first episodes, and then it oscillates slightly, showing an increasing trend up to the final episode. Note that the maximum reward would be equal to times  look_back + 1 = 1167 in the case of predicting correctly at all times.
[ 382 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
Evaluating the model The capability of replicating the trading strategy is measured as matching the position predicted by the agent against the position decided by the strategy. Therefore, the quality of the results does not reflect the strategy's performance in terms of profits and losses. Let's take a look at how to evaluate the model: 1. After training our model, we are ready to evaluate its performance by backtesting. First, we make predictions using the test dataset. To do this, we shall create a function named evaluate() that takes the agent, the input dataset (dataX), the trading decision according to the MACD strategy (dataY), and look_back as arguments: def evaluate(agent, dataX, dataY, look_back):
This function will return a vector containing the positions predicted by the agent at each state. The entire code looks very much like the implementation of the .run() function and can be found in this book's GitHub repository. 2. Now, we can call the evaluate() function from the main script by using the testing dataset: # Evaluate the model y_pred_test = evaluate(agent, X_test, y_test, look_back)
3. The next step is to evaluate the performance of the model in terms of accuracy. We can use the scikitlearn library to do this, as follows: from sklearn.metrics import accuracy_score # Calculate and print accuracy acc = accuracy_score(y_test[look_back1:1], y_pred_test) print('Accuracy: %.2f %' % (acc*100))
Here's the output: Accuracy: 83.27%
This means that our model provides around 83% trading decisions matching with the strategy that we are trying to mimic. This result isn't bad, but how are predictions distributed over the trading positions? This problem can be seen as a multiclass classification problem. Due to this, we need to calculate the precision, recall, and F1 score.
[ 383 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
4. We shall use the scikitlearn library to do this and print the results using a pandas DataFrame: from sklearn.metrics import precision_recall_fscore_support # Calculate and print precision, recall, f1 score and support p, r, f1, s = precision_recall_fscore_support(y_test[look_back1:1], \ y_pred_test, average=None) results = pd.DataFrame({'1Precision': p, '2Recall': r, '3F1 score': f1, \ '4Support': s}, index=le.classes_) print(results.round(decimals=3))
The output is as follows: 1.0 0.0 1.0
1Precision 0.94 0.76 0.88
2Recall 0.69 0.90 0.92
3F1 score 0.80 0.82 0.90
4Support 97 121 63
These measures give us the following information: The high precision values for the 1 and +1 positions tell us that, in almost all cases, the model generates the short and long positions correctly. Instead of the hold position, the recall has a high value, showing that the model is able to correctly predict almost all 0 positions. 5. It may be interesting to visualize the predicted trading positions against the positions given by the strategy. First, we need to decode the decision arrays using the .inverse_transform() method of the le object: # Decodificate labels y_true_test = le.inverse_transform(y_test[look_back1:1]) y_pred_test = le.inverse_transform(y_pred_test)
6. Now, we can plot both the predicted and the true strategies: # Plot strategy plt.figure() plt.plot(y_true_test, label='true') plt.plot(y_pred_test, label='pred') plt.legend() plt.show()
[ 384 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
The output is as follows:
The preceding graph confirms that the errors on predictions are mainly due to the long (+1) and short (1) positions that the model predicts as hold (0).
Summary In this chapter, we implemented a robot trader that's able to learn trading rules by looking at the relationship between market indicators and decisions undertaken regarding entering or quitting a position. The trading rules are learned by using RL – more specifically, the deep Qlearning model. First, we learned how to formulate an RL problem by properly defining the states of the environment, the possible actions that could be taken by the agent, and a reward system. Then, we discovered how to calculate technical indicators using the TALib library in order to determine the market environment and the trading strategy to be replicated. We learned how to implement the deep Qlearning model by designing a basic agent that is trained using the experience replay technique to avoid instability in the model. Finally, we trained and evaluated the model in terms of accuracy, precision, and recall.
[ 385 ]
Training Trader Robots Using Deep Reinforcement Learning
Chapter 12
The results, even though they're only preliminary, suggest that it is possible to learn and perform the complex human behavior exhibited by traders in making a financial decision. Another interesting application of RL may be to learn a trading strategy by discovering the combination of technical indicators that maximizes the final profit. In the next chapter, we will conclude and summarize the different topics we looked at in this book. In addition, we will discuss some aspects we did not cover and new possibilities that are emerging in the field of AI in finance.
Further Research If you want to find out more about the topics that were covered in this chapter, it's recommended that you do the following: Research the effect of the number of episodes, look_back, and batch_size on the efficiency of the model. Research training the model using the other stocks belonging to the NASDAQ100 index. Create an analogous model to mimic another trading strategy, or use different technical indicators as input features.
[ 386 ]
13 What Next? This brings us to the end of our short journey into the world of deep learning in Finance. Throughout the chapters in this book, we have gained experience on how to build models that can guide investment decisions, allocate resources, and trade and assess risk through deep learning's ability to build complex networks of relationships, often nonlinear, that are able to decode and benefit from the market information. Obviously, this is only an introduction and there is still much to discover and experiment with. In this last chapter, we'll try to answer some questions that many of you may have already answered while reading this book. We hope that this can act as a stimulus for further study. In this last chapter, we'll cover the following topics: Automating, discovering and learning models from data Distributing computations across multiple computers and GPUs Exploiting deep learning for highfrequency trading Using deep learning in other FinTech applications Concerns about risks and the future of deep learning in finance
What Next?
Chapter 13
Automating discovering and learning models from data As we have experienced during project development, model development is a complex and intense activity that requires patience and involves aspects of data selection, model construction, model optimization, and the critical interpretation of results. More specifically, as we highlighted in Chapter 3, Construction, Testing, and Validation of Models, the development of a working hypothesis requires patience and entails the following: Retrieving data, selecting data that is deemed useful for the purpose Preprocessing, cleaning, and integrating data to remove noise, errors, and omissions, and finally making it conform to the needs of the model Representing data in an appropriate space of characteristics, especially if they are not originally quantitative (for example, natural language text) Defining a deep learning (DL) architecture for the model you want to develop Training the model Optimizing the model with respect to hyperparameters Testing and validating the model by critically analyzing results If we have time, comparing multiple working hypotheses The need to explore different mixes makes this activity particularly timeconsuming and resourcedemanding. As a response to the need to make model construction and development more effective, in recent times automated machine learning (AutoML) has emerged as a trend of great interest. Its stated goal is simple: to discover how to solve a machine learning problem by means of machine learning. To achieve this goal, it is necessary to fully automate the pipeline, as shown in the following diagram:
[ 388 ]
What Next?
Chapter 13
Figure 1: A typical machine learning workﬂow
When exploring the space of possible models with AutoML, there are two aspects to consider in particular, as follows: Research into the optimal structure Parameters Structure research addresses the problem of identifying the architecture of a deep neural network when considering the various possibilities in relation to the types of block/layer and their characteristics in functional and dimensional terms—for example, whether or not to use convolutional layers and, if so, how many, and with what characteristics in terms of the number of filters, kernel size, striding, and padding. For a totally connected layer, the number of units and the type of activation function need to be taken into account. All these possibilities are what a search algorithm considers when varying the architecture of the model to be considered. In many cases, an architectural structure is given for the neural network and we leave the algorithm to optimize the hyperparameters of that structure. To solve this task, you can use what we have already described in Chapter 3, Construction, Testing, and Validation of Models.
[ 389 ]
What Next?
Chapter 13
As shown in Chapter 2, Designing Neural Network Architectures, and Chapter 3, Construction, Testing, and Validation of Models, once the structure has been decided on, we can move on to training in order to identify the best instance of the model. The model is assessed with respect to a validation set against other alternatives. It should be noted that the training process also has a set of hyperparameters that can be optimized, such as the number of epochs, the learning rate, the learning decay, the momentum, and the batch size. Therefore, it is possible that the learning process can be repeated by considering different options and evaluating the result of each of them, obviously choosing the best one. As you can see, although AutoML automates many steps that are usually left to humans to explore, this process can be particularly expensive in terms of computing resources. The intrinsically parallel nature of exploration makes it possible to increase the number of hypotheses that are evaluated at the same time, provided that we have a data center powerful enough to dedicate to that purpose. Research can be significantly improved if the research process takes appropriate earlystopping conditions that are capable of interrupting those learning processes that have a low probability of improving the models that have been identified so far. Although the ultimate goal of AutoML is to democratize ML technology, and DL in particular, to nonMLexperts, when it comes to simplifying and automating the steps for model analysis, development, and training, it is a particularly useful tool that can speed up the verification and validation of working hypotheses. So, in the last few years, we have witnessed the rapid development of readytouse AutoML tools and frameworks. A resource page showing where to find AutoML tools is https://www.automl.org/automl/. A few of the most interesting ones are as follows: AutoWEKA: AutoWEKA (Waikato Environment for Knowledge Analysis) provides a platform for the optimization of machine learning models through the use of the learning algorithms provided in WEKA. More precisely, AutoWEKA analyzes learning algorithms and related hyperparameter spaces in an inclusive way to generate the best model, given a specific dataset, through the Bayesian optimization process. Starting from version 2.0, it supports optimization processes for both classifiers and regressors with all the metrics provided in WEKA, with the possibility to enable parallel executions (on a single machine) that search for the best N configurations. The execution of the optimization process is governed by two parameters: memorylimitations and the time budget (1 GB and 15 minutes by default). AutoWEKA is accessible via the GUI, commandline interface, or public API.
[ 390 ]
What Next?
Chapter 13
TPOT: The Treebased Pipeline Optimization Tool (TPOT) is a Python tool for the automation of machine learning processes based on genetic algorithms. It is based on the scikitlearn library, which allows us to define machine learning models to be optimized. TPOT allows us to manage the actual search time, providing us with the option to stopping the execution and obtaining a suboptimal solution. Preconfigured working solutions offer a quick approach to many problem cases. Finally, for very large tasks, there is the option of easily distributing the workload on a cluster using the parallel computation support provided by the Dask library. AutoKeras: AutoKeras is an AutoML Python tool that provides functions for network architecture search (NAS) and hyperparameter optimization of these architectures. It works with Keras models and requires only the definition of the problem to be considered and the dataset in question (for example, image classification over the MNIST dataset). The search process is performed by the Bayesian Optimization algorithm with GPU computing. This process consists of evaluating the performance of models with increasing complexity until the time limit, which is defined in the search function invocation, is reached. DEvol: DeepEvolution (DEvol), is a Proof of Concept, based on the Keras framework, that uses genetic architectures to define models in classification problem optimization. Each model is represented as an encoding of genetic information of fixed width based on the neural network characteristics. The main object, the GenomHandler, defines and manages the conversion and mutation of information, and also imposes constraints in terms of the definition parameters of the neural model to be used. Hyperas: Hyperas provides a wrapper around hyperopt to ensure a usable hyperparameter tuning interface, without requiring any knowledge or use of hyperopt syntax. Hyperas works on Keras models; all we have to do is indicate which hyperparameters to optimize and the range and probability distribution to use in order to select the values to test. The tuning process can be configured to run multiple models in parallel, using MongoDB to collect intermediate results. Hyperas allows you to parameterize the number of model levels, the different activation functions, and the various optimizers and hyperparameters. Finally, it returns the combination of parameters that produced the minimum loss value. It can be executed on a GPU.
[ 391 ]
What Next?
Chapter 13
Talos: Talos offers an alternative workflow to Keras, based on the optimization of hyperparameters and model evaluation; it inherits Keras' syntax and functionalities, ensuring full compatibility and speed in its implementation. Optimization is performed on a set of about 30 hyperparameters and configurations that use grid search, probabilistic optimizers, and different search options. You can customize strategies and a live monitor and analytics service is provided. Moreover, there is support for GPU and multiGPU computation. AutoNet: AutoNet is a framework for the automatic tuning of deep neural networks on a given dataset. The search process includes structural parameters of the network and the hyperparameters of each module. When setting hyperparameters, it is possible to choose from predefined ranges. It combines Bayesian Optimization and HyperBand (BOHB) algorithms and uses PyTorch as a deep learning framework. BOHB is particularly efficient as it can direct execution toward more promising models, leaving out models with poor performance. Anh Truong et al. (2019) have published an interesting overview of existing software, some of which were provided in the preceding bullet point list. It provides a comparison in terms of functionality. This has been collated into the following table:
[ 392 ]
What Next?
Chapter 13
[ 393 ]
What Next?
Chapter 13
Distributing computations across multiple computers and GPUs Deep learning's ability to learn complex relationships between data has a cost: you need to repeat the model over a massive amount of raw data, which can be in terabytes or petabytes. Networks also exhibit large parametric spaces, ranging from AlexNet's 0.60 M to over 50 M, which reach modern networks. The number of neurons can quickly exceed 1 billion. All this requires high processing and memory capacities, which often exceed the capabilities of a single Graphics Processing Unit (GPU) and require highperformance computing (HPC) equipped with efficient data processing and storage provided by massive parallel architectures, low latency networks, and highbandwidth memory. In other words, deep learning is a problem for supercomputing today. This trend has been outlined in a recent survey (T. BenNun and T. Hoefler, arXiv:1802.09941) with reference to scientific publications that have appeared over the years. The survey shows how the use of GPUs has grown over time at the expense of the Central Processing Units (CPU), Field Programmable Gate Arrays (FPGA), and other special architectures. At the same time, the use of distributed systems has also increased. The size of such systems varies from a few units to thousands of nodes, as shown in the following diagram. In general, in recent years, we can observe the use of increasingly larger clusters. This is also due to the greater maturity of software frameworks that are mainly based on the MPI paradigm.
Distributed deep learning The use of very complex deep learning models with a large number of parameters results in a very heavy computational load for training. This drives efforts toward the organization of distributed infrastructures equipped with multiple GPUs. Distributed deep learning is a subset of the more general area of distributed machine learning that aims at engineering, building, and executing machine learning models on clusters of distributed machines. A typical multiGPU multinode environment can be seen in the following diagram. A master node can be used to distribute data, allocate tasks, and share parameters during training. Because a master represents a single point of failure (SPOF), it is often redundant, even though this poses bandwidth issues. Other schemes prefer pointtopoint communications to make the system more robust and reduce bottlenecks that are represented by access to the master:
[ 394 ]
What Next?
Chapter 13
Figure 2: MultiGPU multinode environment
The discipline of distributed deep learning presents, together with the classic problems of distributed architectures, other categories of interest: model consistency and parameter distribution. Model consistency ensures that each network component always observes updated network parameters. To achieve this condition, it is necessary to synchronize the parameter updates of all distributed optimizers. The desired degree of distribution of the parameters involves different design choices. If you go for a strongly centralized network architecture, all the parameters will be stored on a parameter server. On the other hand, the parameter matrix will be divided between nodes and it will be necessary to manage its consistency. The idea of distributing the workload related to processes that are necessary for the execution of neural models stems from a twofold need: To distribute the huge computational load on a largescale architecture in order to obtain a higher bandwidth To perform processes based on the data locality principle, in order to minimize data transfer
[ 395 ]
What Next?
Chapter 13
This need is concretized through three partitioning strategies: inputbased, network architecturebased, and layerbased, as shown in the following diagram:
Figure 4: Partitioning strategies for distributed deep learning
Data parallelism The strategy of databased parallelism involves replicating the neural network architecture and partitioning datasets so that they're processed in subsets (minibatches). Each worker implements the training process autonomously on his or her own batches and generates a correction of its own parameters. Then, each worker shares parameters on a parameter server. Models based on data parallelism are particularly efficient when data is distributed on several nodes and it is preferable to implement a processing process inplace, without the need to move large amounts of data. This idea is completely in agreement with the principles of big data processing.
[ 396 ]
What Next?
Chapter 13
Model parallelism The model parallelism strategy consists of distributing the neurons of each layer over the nodes. The data is then copied on each node, which computes a different segment of the deep neural network. The structure of DNNs has innate dependencies between the neurons, so the training process involves a huge communication effort to synchronize the various sectors of each layer. This communication problem generates inefficiencies, especially in the presence of fully connected layers and/or convolutive ones, so one solution to mitigate this problem is to give each worker twice as many neurons, with some overlapping, in order to take more time for computation and less time for communication. This approach is convenient when the network is very extensive where, because the memory of a single node is not enough to hold the data and the whole parameter matrix, the latter is divided into submatrices and distributed on the nodes of the cluster.
Layer pipelining The pipelinebased parallelism paradigm is where we start processing a layer when just a part of the output data from the previous layer is available. In this way, layers can be distributed on the various nodes, without the need to share parameters. This approach enables the communication of output data as soon as possible from one node to the next in the pipeline and helps minimize the idle time of the computing units. The pipeline structure ensures that there are a defined number of communication points and that the actors are defined in advance.
Frameworks for deep learning The need to distribute the computational load during network training has led to the creation of specific frameworks. With the increasing amount and complexity of data, the use of these frameworks becomes almost obligatory. Alongside solutions that were initially based on generalpurpose platforms such as Hadoop and Spark, adhoc proposals for deep learning have been developed over time. Some of the main ones will be presented in this section.
[ 397 ]
What Next?
Chapter 13
Horovod Horovod is a Python framework that supports the distribution of deep learning models on clusters. Being integrated with TensorFlow allows you to deploy training a complex model with minimal scripting effort. The communication paradigm that's used is the Message Passing Interface (MPI). It exploits data parallelism natively: each node performs a training step on its own data subset, updates the parameters, shares them with other workers, and averages them according to the ringall reduce algorithm. Then, it repeats.
Distributed TensorFlow models Distributed TensorFlow models run on clusters of GPUs, where nodes are divided into workers and parameter servers. The workers train their dataset and communicate weight updates to the parameter server, which, after receiving the updated weights from each worker, aggregates and broadcasts them. One of the disadvantages of using Distributed TensorFlow is that it explicitly manages the starting and stopping of the parameter servers, but this can be mitigated by using a cluster manager, such as YARN. Distributed TensorFlow can also run in the context of SPARK applications. More specifically, you can configure the cluster to use RDDs to manage a worker's input, or use TensorFlow's native mode, which allows us to efficiently read data from distributed filesystems.
BigDL BigDL is a framework for distributed deep learning that was designed in agreement with big data processing principles. It enables the execution of deep learning tasks on the same cluster where the data is stored and allows us to integrate deep learning functionality with analyticsbased workflows. The BigDL approach provides support for the design of workflows that focus workloads related to big data processing and deep learning models on the same architecture, without the need to implement adhoc connectors that enable communication between these two types of process. This leads to the creation of deep learning pipelines that have been fully integrated with data analytics, which can then be executed as Spark jobs.
[ 398 ]
What Next?
Chapter 13
Elephas Elephas is an extension of Keras that aims to integrate the execution of deep learning models in distributed mode on Spark clusters with Keras. The algorithms that are implemented in Elephas use RDD and DataFames. The Keras model is initialized by the driver and then serialized and sent to the workers, who train it on their data chunk and send weight updates to the driver. It uses them to update its "master" model with an optimizer that can receive worker updates synchronously or asynchronously.
Exploiting deep learning for highfrequency trading In recent years, highfrequency trading (HFT) has been subject to great interest and popularity. The idea of being able to earn by arbitrating the spread between the bid and ask on the price of shares, thanks to complex and powerful algorithms capable of trading in and out positions in a fraction of a second, has attracted the attention of traders who can accumulate millionaire profits from tens of thousands to millions of transactions. Paradoxically, HFT's great success also marked its demise. In fact, the arrival of many traders in this type of trading has significantly reduced profit margins. According to a data collected by TABB Group, a financial services industry research firm, returns from US equities were, for the first time, below 1B in 2017, compared to over 7.2B in 2019, although the level of activity has remained between 50% and 60% of the total trading volume over the years. In fact, the decline in HFT reflects the growth in algorithmic trading, which rose from 30% to over 90% in a few years in some markets. The speed of execution is certainly the most critical factor in HFT, particularly in terms of the overall time, which takes into account the delay with which the order book is acquired and the time it takes to make a decision and turn it into an order that is transmitted to the market. Thus, this technological component has always played a key role in HFT's success. The possibility of positioning powerful servers connected to highspeed networks close to the market has represented (and still represents) a barrier to the entry of new operators. However, the competitive advantage given by the use of more powerful hardware, highspeed, lowlatency communication lines, and highly efficient algorithms has been eroded as more and more players have been able to access them. This poses new challenges and a question remains as to whether deep learning can actually help in defining new and more sophisticated trading strategies.
[ 399 ]
What Next?
Chapter 13
An HFT system bases its effectiveness on its ability to accurately predict the price in an interval after the current time, and this must be achieved in an incredibly short time. To achieve this, HFT systems generally employ very simple heuristics, such as considering movements whose price exceeds the spread between bid and ask, or by simplifying the problem, such as assuming movements while ignoring the time they occur, or even decisional elements, such as choosing which suggestions to follow or not based on the price confidence interval. All these simplifications lead to ignoring the strongly negotiated nature of micromovements and their origin within broader and more articulated strategies, which can also be aimed at hiding broader orders that can potentially change the market's orientation. To understand the nature of this type of trading, consider a money market with a set of three currencies; for example, USD, EUR, and JPY. The relationship between these currencies is consistent in the sense that the exchange rate between two currencies should be such that, the net of costs, 10 USD in EUR, then EUR in JPY, and finally JPY in USD should not return the initial 10 USD. However, since the exchange rate is between currency pairs, it is possible that there are temporary mispricings that can be arbitrated for profit. For example, if USD/EUR = 0.91, EUR/JPY = 121.85, and JPY/USD = 0.0092, the initial 10 USD becomes 10.20 USD, with 20 cents' profit. The strategy appears simple: execute exchanges in succession on a basket of currencies if the ratio of return to the original currency is greater than 1. In practice, the speed of execution and exchange risk management must be considered. Compared to the former, the algorithm must be followed quickly with low latency. The latter is more complex since, during the exchange operations, the prices could vary, creating no arbitrage opportunities. For this reason, it is necessary to ensure that all exchanges are executed simultaneously with the desired volumes. In addition, due to the small price variations and the high number of transactions, order execution costs must be taken into account by the HFT algorithm. Understanding that the critical factor in an HFT system is the speed of execution, deep learning, thanks to its ability to learn complex relationships between even heterogeneous data, can still be used for a number of purposes. It can be used to define a simulative model of market behavior at the microtransaction level, for example, using GANs, as we learned in Chapter 9, Risk Measurement Using GANs. In this case, we can try to define a model that replicates the price dynamics of an order book, a behavior that would be difficult to replicate with conventional Monte Carlo simulations based on a parametric statistical approach. This would allow us to test HFT trading strategies in a controlled environment.
[ 400 ]
What Next?
Chapter 13
A second use could be to replicate the inputoutput relationship of a decision model that is too complex to be executed with strict realtime constraints. In this case, it would be important to optimize the inference time of the model while considering the technological constraints related to integration with devices such as the GPU, which could still make the solution incompatible with the execution time available. Further use would be linked to the possibility of using deep reinforcement learning, which we presented in Chapter 12, Training Trader Robots with Deep Reinforcement Learning, to train a robot to operate in HFT by selecting a winning strategy. This is now a field of particular interest and promises to offer new competitive advantages to operators in the sector. Finally, DL can be used to build a model that's capable of directing strategy decisions by binding heterogeneous data, as in the case of Natural Language Processing (NLP), that's then applied to the analysis of unstructured data in text form. This could influence the market regarding news, business prospectuses, graphics, and more, as shown in Chapter 8, Digesting News Using NLP with BLSTM, and Chapter 11, Better Chart Analysis Using Capsnet.
Using deep learning in other FinTech applications In Chapter 1, Deep Learning for Finance 101, we briefly outlined the possible applications of AI to finance; that is, applications that go beyond the development of investment and trading strategies, as described in this book. Financial Technology, commonly known as FinTech, is an innovative technology that aims to deliver financial services more efficiently in order to reduce costs. The key areas where FinTech has made inroads are as follows: Payment transfer and processing Robo advisory Product comparision Insurance Risk management Mortgage origination and approval Alternate currency
[ 401 ]
What Next?
Chapter 13
FinTech relies on discerning information from a variety of data sources and pattern recognition systems so that it can be translated into rules. Deep learning frameworks are well suited to the aforementioned applications, where data is a combination of structured and unstructured data. The techniques and methods we described in Chapters 412 can be reused in different contexts. For example, Chapter 9, Risk Measurement Using GANs, talked about how risk measurement is a building block for the risk management framework and can be suitably modified for better risk management. We'll go over some of the aforementioned applications in more detail in this section.
Payment transfer and processing Fraud detection is a critical function for a payment transfer firm to have. The ability to spot circuitous transactions camouflaging illicit activity is of paramount importance. Palantir's systems, which integrate data from all transactions in order to visualize transaction pathways, are a great application of machine intelligence techniques. Credit card companies employ NLP techniques to flag outoftheordinary transactions, which are then passed through a second Convolutional Neural Network (CNN) to check the pattern recurs at regular intervals. If both these filters signal that a transaction is fraudulent, it is then followed up and verified by a personal call. This feedback is fed into the CNN to improve its accuracy and update its rules. Hence, a continuous improvement framework makes systems more efficient by relying less on human intervention and thus early detecting fraud and alert cases.
Robo advisory Each individual's need for financial planning is different, so they are willing to pay high fees to financial advisors to manage their portfolios. Robo advisory firms have started to use technology more effectively to offer customized advice at a lower cost. Robo advisors classify investors and products into categories based on a large number of criteria. This combination of criteria is nonlinear, leading to better classification. Supplementing this with recommender systems, good decisions can be easily cascaded across all investors and strategies can be devised to purchase financial products at a lower cost due to aggregation. A robo advisor decides when to rebalance an individual's portfolio in a systematic fashion depending on the following criteria: the return objective, risk constraints, changing macro and microenvironments, and a number of other factors. In a similar manner, the same techniques for classification and prediction can be applied to other aspects of FinTech.
[ 402 ]
What Next?
Chapter 13
Alternate currencies Bitcoin is the most popular blockchain application and pushed the technology from geek forums to the front pages of all media. The soaring prices of Bitcoin, which surpassed $18,000 in 2018 as a currency with a finite supply that's not subject to manipulation by central banks, has enchanted scammers and sophisticated coders alike. This has led to a proliferation of alternative currencies as a digital means of storing value. The deep learning techniques that have been applied to these markets are still evolving, which makes it a rewarding area of research.
Concerns about risks and the future of deep learning in finance Deep learning technology certainly offers new research opportunities for the financial sector, especially in a period of low or negative interest rates, as is currently the case. However, it must be taken into account that, although effective, deep learning models are not infallible. The high processing capacity and speed of execution of trading algorithms can lead to high losses, as some recent cases have shown both in relation to individual firms and the whole market. Two remarkable cases highlight this risk. On 1 August 2012, Knights Capital sustained a loss of over 440 M in less than an hour due to a trading glitch. 2 years earlier, on 6 May 2010, a flash crash of S&P 500, Dow Jones Industrial Average, and Nasdaq Composite indices collapsed the market for no apparent reason by approximately 9% in about 36 minutes, with a capitalization loss of over a trillion dollars. The inability to fully understand the highly nonlinear dynamics governing deep neural network models places greater limits than in the past on human intervention in controlling systems based on this technology. As long as predictions are accurate, this is not a significant problem. However, when a single case occurs, highspeed and hightrading volumes can lead to significant losses, and understanding the reasons or the single item of information that produced them may be difficult, even for those who created the models. In this sense, however extensive a model validation may be, it may be inadequate to cover and reproduce all realworld scenarios in which the model may be operating.
[ 403 ]
What Next?
Chapter 13
Explainable AI (XAI) is certainly an approach that is gaining interest in decisioncritical areas such as the medical, legal, and financial fields. In these areas, the problem of interpretability, that is, understanding how output has been produced from the input provided, is of fundamental importance. It must also be said that this problem is typically posed postmortem following an accident in order to improve the system. This does not limit the operational risk associated with the opacity of such models when they're used in highspeed trading systems. One research direction in the industry is the use of XAI models, which can verify a decision in an input environment in search of possible factors that may have polarized it. This is done in order to determine how robust that decision actually is and whether the behavior expressed by the system complies with risk management constraints. In addition to the risk of producing inaccurate predictions, there are two other problems that should be considered. The first is linked to the lower diversification of the strategies that could be derived from the adoption of common models induce the algorithms to exhibit homogeneous behaviors that can artificially amplify the trend of an upward, but above all downward, market. If all the main operators adopt the same model, it is to be expected that the decisions that are taken are similar and that this could inevitably lead to creating an excess of demand or supply on the market that's not justified by the appropriate economic fundamentals. The second problem is the possibility of neural network hacking. Recently, several researchers have highlighted how it is possible to mislead a network and even control its behavior using appropriate input patterns. This poses questions about the possibility of market manipulation by acting on the logic of neural networks when, one day, they will be widely spread in trading activities. The advent of AI technology in the financial world is profoundly changing the landscape. Together with the immense advantage that is being offered by incorporating deep learning, unprecedented threats may arise. We could call them cyberswans. This term refers to the possibility that an institution or a market may run into an event that, although it only exists in the complex abstract logic of models, has inevitable consequences for financial activity, with possible economic impacts. This must attract the attention of operators and regulators so that they can determine rules that can minimize risks deriving from the increasing adoption of these technologies. Unfortunately, looking at the scant attention that the sector generally gives to facing changes leads us to believe that an accident with potentially serious repercussions will be necessary before the regulator can intervene to impose new and higher standards for the adoption of new technologies. This is mainly due to resistance to new market regulations and the limited attention that's paid to investing equal resources into the development of new models to manage the resulting new risks.
[ 404 ]
What Next?
Chapter 13
Concluding remarks The era of deep learning in finance is only in its infancy. Being dataintensive, digital, global, and based on quantitative decisions makes modern finance a natural field of deep learning. The possibility of learning about complex relationships, usually nonlinear, between data allows us to obtain great benefits in defining models. These are useful for defining investment and trading strategies, as well as the discovery of new opportunities for returns, especially in a period of low rates like ours. However, there is still a lot of work to be done and it is not enough to simply transplant techniques that have been tested with resounding success in other fields such as computer vision, robotics, speech analysis, natural language processing, and others. The reason lies in understanding that the nature of data and tasks differs significantly from other areas such as those we just mentioned. The first consideration in this respect is related to the observation space; that is, the domain where the input data resides. In the case of images or sounds, the phenomenon that is analyzed is fully expressed within the data that is recorded and supplied as input to the model. Instead, in the case of finance, the input data is only a part of the complex dynamics that lead to formulating a demand and an offer and therefore to cross them in the market. Surely an efficient market hypothesis makes the price the best expression of such dynamics, but often the picture we get is only partial, for a number of reasons related to the different forces operating on the market. For example, an order could refer to a single purchase or be part of a wider strategy. Iceberg orders, also called reserve orders, are large orders that are divided into smaller limit orders, often through the use of algorithmic trading. The fact that they are called iceberg orders is due to the fact that what is observed is only the tip of the iceberg. More generally, there is a myriad of different types of orders. Only a few encoded types arrive on the market and the ability to cross them determines their price, but the dynamic arises upstream in the strategy that determines the original orders. In this, the world of finance is united in the elaboration of a natural language in an attempt to understand the meaning and thought behind the production of words. In the case of financial markets, there are often many voices that speak and often with similar but distinct dialects; that is, the ways in which each operator has to act in the market. Finally, in the market, everyone acts in different ways and for different purposes, and this complicates the analysis scenario.
[ 405 ]
What Next?
Chapter 13
Another aspect concerns the repetitiveness of patterns. In images and sounds, such patterns exist and tend to repeat themselves. Moreover, when they are aggregated into more complex patterns, they actually serve to build distinct but distinguishable objects. This is one of the fundamental aspects of how convolution works. In the case of finance, although patterns obviously exist, they do not always refer to the same phenomenon and do not always aggregate in the same way. Therefore, we can state that the same phenomenon, suppose is a trend, can be manifested with different observations, for example, the sequence of the prices or returns, and the same observations could refer to phenomena, that is, the trend, differently. In other words, the past tends to be repeated in the future, but this depends on what we intend to observe. Therefore, deep learning will be able to keep its promise and produce a generational shift in the development of tools if it is able to capture and interpret (as far as possible) regarding the complexity of the financial reality. This will require it to integrate several sources of heterogeneous data, some primary and others secondary, while attempting to decode a dense network of relationships in order to understand the principles governing the dynamics of markets. The main market players have started to invest significant resources in the implementation of their AI programs. For example, BlackRock is one of the first to have a specific program for the internal adoption of AI. This program is based on three main directions of development: The establishment of an internal lab with the aim of achieving a greater and better diffusion of AI technologies within the company The creation of a data science group with the intention of using AI for the resolution of some high priority issues Automation through an AI of backoffice activities in order to pursue process improvements in terms of performance, cost, and reliability JPMorgan is also very active in this: Its AI initiative is oriented toward the resolution of specific tasks, such as processing natural language to support trading activities. Anomaly detection, for recognizing fraud or risk mitigation. Other large banks and hedge funds have similar programs. In this book, we have tried to illustrate some possibilities that deep learning offers in addressing typical problems in finance. You are invited to continue experimenting and try and build models and solutions in order to understand the assumptions and implementation choices of the model. Your imagination is the only limit!
[ 406 ]
Other Books You May Enjoy If you enjoyed this book, you may be interested in these other books by Packt:
Machine Learning for Finance Jannes Klaas ISBN: 9781789136364 Learn how to clean your data and ready it for analysis Apply machine learning to structured data, natural language, photographs, and written text Understand how machine learning can help you detect fraud, forecast financial trends, analyze customer sentiments, and more Implement heuristic baselines, time series, generative models, and reinforcement learning in Python, scikitlearn, Keras, and TensorFlow Delve into neural networks, and examine the uses of GANs and reinforcement learning Debug machine learning applications and prepare them for launch Address bias and privacy concerns in machine learning
Other Books You May Enjoy
Learn Algorithmic Trading Sebastien Donadio, Sourav Ghosh ISBN: 9781789348347 Understand the components of modern algorithmic trading systems and strategies Apply machine learning in algorithmic trading signals and strategies using Python Build, visualize and analyze trading strategies based on mean reversion, trend, economic releases and more Quantify and build a risk management system for Python trading strategies Build a backtester to run simulated trading strategies for improving the performance of your trading bot Deploy and incorporate trading strategies in the live market to maintain and improve profitability
[ 408 ]
Other Books You May Enjoy
Leave a review  let other readers know what you think Please share your thoughts on this book with others by leaving a review on the site that you bought it from. If you purchased the book from Amazon, please leave us an honest review on this book's Amazon page. This is vital so that other potential readers can see and use your unbiased opinion to make purchasing decisions, we can understand what our customers think about our products, and our authors can see your feedback on the title that they have worked with Packt to create. It will only take a few minutes of your time, but is valuable to other potential customers, our authors, and Packt. Thank you!
[ 409 ]
Index A activation function 43 AdaDelta 60 AdaGrad 60 Adaptive Moment Estimation 60 AE variants denoising AE 162, 163, 165 exploring 162 sparse AE 166, 167, 168, 169 AI, in finance automatic reasoning 19, 20, 21 computing probabilities 15, 16, 17, 19 expert systems 22, 23, 24 history 12 machine learning at scale 27, 28 narrow AI systems 25, 26 shared background 13, 14 American Airlines Group Inc. (AAL) 369 Applied Expert System (APEX) 22 artificial intelligence (AI) financial services industry, redefining 10, 11, 12 Artificial Neural Network (ANN) 43 artificial neurons 43 asset stratification 206 AutoNet 392 AutoWEKA (Waikato Environment for Knowledge Analysis) 390 autoencoder (AE) about 29, 53 implementing 32, 33, 35, 38 implementing, with TensorFlow 38, 40, 41 used, for index replication 156, 157, 159, 161 AutoKeras 391 automated machine learning (AutoML) 388 Automatic Computing Engine (ACE) 19 AutoML tools
AutoNet 392 AutoWEKA 390 AutoKeras 391 DeepEvolution (DEvol) 391 Hyperas 391 reference link 390 Talos 392 Treebased Pipeline Optimization Tool (TPOT) 391 Autoregressive Moving Average (ARMA) 195
B Back Propagation Through Time (BPTT) 93, 242 backpropagation reference link 77 BagofWords (BoW) 257 Bayesian Optimization and HyperBand (BOHB) 392 benchmarking about 141 alternative models 211 to model diagnostics 142 bidirectional LSTM (BLSTM) implementing 267 BigDL 398 binary classification 272 Bitcoin 403 BlackRock 406 BLSTM model creating 270, 272 data preparation 268, 269 evaluating 273, 275, 277 fitting 270, 272 implementing 267
C Caffe2 about 116 cons 117 pros 117 Caffe about 119 cons 120 pros 120 Canadian Institute For Advanced Research (CIFAR10) 344 capitalizationweighting 146 CapsNet model constructing 344, 347 data, setting up 347, 350, 352 implementing 347 training 352, 353, 356, 359, 361 CapsNets about 339, 341 advantages 343 disadvantages 344 dynamic routing, between capsules 342 matrix capsules, with EM routing 343 modeling 341 Chainer about 118 cons 118 pros 118 Chicago Board Options Exchange (CBOE) 176 Client Profiling System (CPS) 22 CNNLSTM model implementing 242, 244, 245, 246 computation distributing, across multiple computers and GPUs 394 Continuous Bag of Words (CBOW) 261 convolutional neural networks (CNNs) about 78, 86, 90, 92, 339, 402 constructing 220, 221 data, setting up in correct format 222, 223, 225 examples, AlexNet 81 examples, LeNet5 81 investment logic, modeling 221 model, testing 231
model, training 225, 226, 228, 230 network architecture, selecting 221 pooling operation 81 corpus 257 country models analyzing 247, 248, 250, 252, 254 testing 246 validating 246 cumulative squared error visualizing 198, 199 cyberswans 404
D data configuration about 368 data preparation 374 data, loading 369 input data 373 trading strategy, defining 369, 372 data parallelism 396 data gathering 147, 148, 149 handling 203 loading 177, 178, 179, 264, 266 obtaining, from public sources 203, 204 setting up 205 setting up, for ResNet50 model 327, 328, 330 splitting 264, 266 Deep Autoencoder (DAE) about 169, 171, 172 structure 170 deep learning model about 240, 241, 242 architecture 241 deep learning, for finance about 29 data, gathering 30, 31 future 404 risks 403, 404 deep learning, frameworks about 397 BigDL 398 Distributed TensorFlow 398 Elephas 399 Horovod 398
[ 411 ]
deep learning, in Fintech applications about 401 payment transfer and processing 402 Robo advisory 402 deep learning for highfrequency trading 399, 400 deep neural network (DNN) 366 deep Qlearning algorithm 366, 367 deep Qlearning model act() method, using 379 agent, designing 375 agent, training 379, 382 DQN 376 evaluating 383, 384 experience replay strategy 377 implementing 375 remember() function, using 377 DeepEvolution (DEvol) 391 DeepNets used, for incorporating memory 108 defineandrun approach 115 DENDritic ALgorithm (DENDRAL) 20 denoising AE 162, 163, 165 discriminator 298 distributed deep learning about 394, 395 data parallelism 396 layer pipelining 397 model parallelism 397 Distributed TensorFlow 398 Dow Jones Industrial Average (DJIA) 30, 146, 177 Dynamic Computation Graph (DCG) 116
E Efficient Market Hypothesis (EMH) 26, 103, 256 Electronic Data Processing (EDP) 19 Electronic Discrete Variable Automatic Calculator (EDVAC) 19 Electronic Numerical Integrator and Computer (ENIAC) 19 elements, RL agent 364 environment 364 model of environment 364 policy 364
reward function 364 value function 364 Elephas 399 epoch 58 Error Correction (EC) 240 ExchangeTraded Fund (ETF) 146 Expectations Maximization (EM) algorithm 343 Explainable AI (XAI) 404
F Factor Models (FM) constructing 106 for return prediction 106 FastText 263 FastText word vectors download link 283 financial data acquiring, from multiple sources 109 characteristics 110, 111 financial models building blocks 101, 102 financial services industry redefining, with AI 10, 11, 12 FinCEN Artificial Intelligence System (FAIS) 24 fraud detection 402 frequencybased word vectors about 257 count vectorization 257 TFIDF vectorization 259
G GAN model Implementing 301, 303, 304, 306, 309, 311 GARCH model 195, 196, 197 Gated Recurrent Unit (GRU) 98, 99 Gaussian Process (GP) 133 General Problem Solver (GPS) 20 Generalized Autoregressive Conditional Heteroskedasticity (GARCH) 175 generative adversarial network (GAN) about 297, 298 discriminative models 299 discriminator network 299 generative models 298 generator network 299
[ 412 ]
results, benchmarking 311, 312, 314, 315 used, for implementing risk model 300 working 297, 299 Global Vectors (GloVe) 264 GoogLeNet 89 Gradient Descent Algorithm (GDA) 57 Graphics Processing Unit (GPU) 394
H Hebb's Rule 62 Hidden Markov Model (HMM) 105 highfrequency trading (HFT) about 12 deep learning 399, 400 highperformance computing (HPC) 394 historical returns method used, for calculating VaR 295 historical volatility 175 Horovod 398 Hyperas 391 hyperbolic tangent function 46 Hyperopt package 133 hyperparameters 64, 70 hypothesis formulation 205, 207, 208, 210 hypothesis, for trading formulating 103, 105
I ImageNet database models 322 ImageNet Large Scale Visual Recognition Challenge (ILSVRC) 85 imbalanced classes dealing with 278, 280, 281 implied volatility 176 insample scoring approach 67 insample testing 205, 206, 208, 210 index replication, methods physical replication 146 synthetic index replication 146 index replication about 146, 147 with AE 156, 157, 159, 161 inductive transfer learning (ITL) 322 information ratio 124
International Business Machine (IBM) 18 intraday volatility 176 investment strategy commonly used financial metrics 122 commonly used statistics 122 cumulative and monthly returns 123 documenting 138 evaluating 122 functioning 135, 137, 140 implementing, costs 105 information coefficient 123 paper portfolios 139 production environment, transitioning 138 soft launch process 139
K Keras about 112 cons 112 layers 49 pros 112 reference link 271 used, for implementing vanilla AE 152, 154, 155 Kringing 133
L Lam Research Corporation (LRCX) 157 layer pipelining 397 LeNet5 81, 83 linear discriminant analysis (LDA) 17 linear models nonlinearity, adding 107 Linear Threshold Unit (LTU) 62 live data benchmarking 141 local receptive field 78 Long ShortTerm Memory (LSTM) 96, 97 LongTerm Capital Management (LTCM) 26 Longterm Recurrent Convolutional Network (LRCN) model 242 LSTM model performance hyperparameters, tuning 191, 192, 193 improving 187 online learning 188, 189, 190 results, visualizing 193, 194
[ 413 ]
stacking layers 190, 191 LSTM model comparing, with other model 194 data preparation 180, 181, 182 evaluating 185, 186, 187 implementing 180 multivariate model, creating 182, 183, 184 multivariate model, fitting 182, 183, 184
M MACD histogram about 369 signals 369 machine learning versus statistical models 109 massive parallel processing (MPP) 27 maximum drawdown 126 maximum likelihood estimation (MLE) 17 Mean Square Error (MSE) 184 Message Passing Interface (MPI) 398 Microchip Technology Inc. (MCHP) 157 Microsoft Cognitive Toolkit (CNTK) about 114 cons 115 pros 115 minibatch 58 model parallelism 397 model tuning about 128 Bayesian optimization 132, 134, 135 grid search 128, 130 random search 131, 132 model discovery, automating 388, 389 implementing 112 learning, automating 388, 389 tuning 128 Modified National Institute of Standards and Technology (MNIST) 343 Morgan Stanley Capital International (MSCI) 238 moving average (MA) 339 Moving Average Convergence Divergence (MACD) 369 multilabel classification 272 MultiLayer Perceptron (MLP)
about 52 neurons, based on distance 71, 73 working with 63, 64, 68, 71 multiclass classification 272 Multiple ADAptive LINear Elements (MADALINE) 62 Multiplicative Error Model (MEM) 175 MXNet about 117 cons 118 pros 117
N Natural Language Processing (NLP) 401 Nestorov momentum 59 network architecture search (NAS) 391 network training, through backpropagation 75, 77 neural networks for capturing nonlinearity and preference shifts 108 history 62 neurons organizing 48, 50 normalization 151 Nvidia Corporation (NVDA) 157
O Open Neural Network Exchange (ONNX) 115 optimizer, usage reference link 184 outofsample approach 66 overfitting 70 oversampling method 278
P parametric method used, for calculating VaR 295 Payment Services Directive (PSD2) 10 Pearson Correlation Coefficient (PCC) 160 piecewise linear function 45 potential term 43 pretrained word embeddings applying 281, 283, 286 prediction quality
[ 414 ]
improving 278 PricetoBook Value (PBV) 240 priceweighting 146 Principal Component Analysis (PCA) 34, 162 problem of interpretability 404 public sources data, obtaining from 203, 204 PyTorch about 115 cons 116 pros 116
R realized volatility 177 Rectified Linear Unit (ReLU) 192 Recurrent Neural Network (RNN) 92, 94, 96 regularization 61 Reinforcement Learning (RL) 118, 364, 365 relevant financial models selecting 105 representational spaces 51, 53, 55 ResNet50 model data, setting up 327, 328, 330 importing 330, 332, 334, 335 training 330, 332, 334, 335 Return on Equity (ROE) 237 risk model defining 300, 301 implementing, with GAN 300 RL problem, formulating about 367 action 367 reward 368 state 367 RNN model 194 routing by agreement 342
S sentiment analysis for finance 256 separate decisions considering 287, 289, 291, 293 Sequential Model Bayesian Optimization (SMBO) 135 shape hyperparameter 71
Sharpe ratio 124 sigmoid function 45 signals trading, with technical indicators 202 simple classification network 214, 215, 217, 218, 220 simple moving average (SMA) 323 simple trading rule 212, 213 single point of failure (SPOF) 394 Sortino ratio 127 sparse AE 166, 167, 168 stacking multiple layers example 55 standard logistic function 45 standardization 180 stateful LSTM 182 stateless LSTM 182 Stochastic Gradient Descent (SGD) 59 stock index 146 strategic asset allocation (SAA) 236 stride 80 support vector machines (SVM) 17
T tactical asset allocation (TAA), problem bets 237 datasets, using for 237 defining 236 individual forecasting 237 joint forecasting, for asset class 237 tactical asset allocation (TAA) data, setting up 238, 239 model, building 239, 240 modeling 236 Talos 392 technical indicators signals, trading 202 TensorFlow about 113 cons 113 pros 113 used, to implementing autoencoder 38, 40, 41 tensors computing with 73, 75 Term Frequency–Inverse Document Frequency (TFIDF) 259, 260
[ 415 ]
test images predicting 335, 336 text data representing 257 Theano about 114 cons 114 pros 114 Threshold Logic Unit (TLU) 62 time stratification 205 Torch about 119 cons 119 pros 119 trading rules 202 transfer functions 43, 47 transfer learning model data, acquiring 325, 326 data, formatting 325, 326 implementing 324 transfer learning about 318, 319, 320, 323, 324 aspects 321, 322 visual inspection, using for technical analysis 322 Treebased Pipeline Optimization Tool (TPOT) 391 Treestructured Parzen Estimator (TPE) 133, 134
U underfitting 70 undersampling method 278 Universal Approximation Theorem (UAT) 67
V validation set 71 value at risk (VaR) calculating, parametric method with parametric method 295
calculating, with nonparametric method 295 computing methods 295, 296 drawbacks 295, 296 estimating 294, 295 vanilla AE data exploration 149, 150, 151 data preparation 149, 150, 151 implementing 149 implementing, with Keras 152, 154, 155 model, evaluating 155, 156 vanishing gradient problem 169 variance–covariance method 295 Variational Autoencoders (VAE) 118 visual inspection using, in visual inspection 322 vocabulary 257 Volatility Index (VIX) 176 volatility, types about 175 historical volatility 175 implied volatility 176 intraday volatility 176 realized volatility 177 Volatility Index (VIX) 176 volatility measuring 174
W weights 55, 58, 61 Winsorization 111 word embeddings about 260 FastText 263 Global Vectors (GloVe) 264 Word2Vec 261 Word2Vec about 261 CBOW model 261 download link 281 skipgram architecture 262 Wrappers 120, 121