출처 : http://the-paper-trail.org/blog/columnar-storage/


Columnar Storage

You’re going to hear a lot about columnar storage formats in the next few months, as a variety of distributed execution engines are beginning to consider them for their IO efficiency, and the optimisations that they open up for query execution. In this post, I’ll explain why we care so much about IO efficiency and show how columnar storage – which is a simple idea – can drastically improve performance for certain workloads.

Caveat: This is a personal, general research summary post, and as usual doesn’t neccessarily reflect our thinking at Cloudera about columnar storage.

Disks are still the major bottleneck in query execution over large datasets. Even a machine with twelve disks running in parallel (for an aggregate bandwidth of north of 1GB/s) can’t keep all the cores busy; running a query against memory-cached data can get tens of GB/s of throughput. IO bandwidth matters. Therefore, the best thing an engineer can do to improve the performance of disk-based query engines (like RDBMs and Impala) usually is to improve the performance of reading bytes from disk. This can mean decreasing the latency (for small queries where the time to find the data to read might dominate), but most usually this means improving the effective throughput of reads from disk.

The traditional way to improve disk bandwidth has been to wait, and allow disks to get faster. However, disks are not getting faster very quickly (having settled at roughly 100 MB/s, with ~12 disks per server), and SSDs can’t yet achieve the storage density to be directly competitive with HDDs on a per-server basis.

The other way to improve disk performance is to maximise the ratio of ‘useful’ bytes read to total bytes read. The idea is not to read more data than is absolutely necessary to serve a query, so the useful bandwidth realised is increased without actually improving the performance of the IO subsystem. Enter columnar storage, a principle for file format design that aims to do exactly that for query engines that deal with record-based data.

Columns vs. Rows

Traditional database file format store data in rows, where each row is comprised of a contiguous collection of column values. On disk, that looks roughly like the following:

Row-major On-disk Layout

This row-major layout usually has a header for each row that describes, for example, which columns in the row are NULL. Each column value is then stored contiguously after the header, followed by another row with its own header, and so on.

Both HDDs and SSDs are at their most efficient when reading data sequentially from disk (for HDDs the benefits are particularly pronounced). In fact, even a read of a few bytes usually brings in an entire block of 4096 bytes from disk, because it is effectively the same cost to read (and the operating system usually deals with data in 4k page-sized chunks). For row-major formats it’s therefore most efficient to read entire rows at a time.

Queries that do full table-scans – i.e. those that don’t take advantage of any kind of indexing and need to visit every row – are common in analytical workloads; with row-major formats a full scan of a table will read every single byte of the table from disk. For certain queries, this is appropriate. Trivially, SELECT * FROM table requires returning every single column of every single row in the table, and so the IO costs for executing that query on a row-major format are a single-seek and a single large contiguous read (although that is likely to be broken up for pipelining purposes). The read is unavoidable, as is the single seek; therefore row-major formats allow for optimal IO usage. More generally, SELECT <col_set> FROM table WHERE <predicate_set> will be relatively efficient for row-major formats if either a) evaluating the predicate_set requires reading a large subset of the set of columns or b) col_set is a large subset of the set of columns (i.e. the projectivity is high) and the set of rows returned by the evaluation of the predicates over the table is a large proportion of the total set of rows (i.e. the selectivity is high). More simply, a query is going to be efficient if it requires reading most of the columns of most of the rows. In these cases, row-major formats allow the query execution engine to achieve good IO efficiency.

However, there is a general consensus that these SELECT * kinds of queries are not representative of typical analytical workloads; instead either a large number of columns are not projected, or they are projected only for a small subset of rows where only a few columns are required to decide which rows to return. Coupled with a general trend towards very wide tables with high column counts, the total number of bytes that are required to satisfy a query are often a relatively small fraction of the size on disk of the target table. In these cases, row-major formats often are quite wasteful in the amount of IO they require to execute a query.

Instead of a format that makes it efficient to read entire rows, it’s advantageous for analytical workloads to make it efficient to read entire columns at once. Based on our understanding of what makes disks efficient, we can see that the obvious approach is to store columns values densely and contiguously on disk. This is the basic idea behind columnar file formats. The following diagram shows what this looks like on disk:

Column-Major On-disk Layout

A row is split across several column blocks, which may even be separate files on disk. Reading an entire column now requires a single seek plus a large contiguous read, but the read length is much less than for extracting a single column from a row-major format. In this figure we have organised the columns so that they are all ordered in the same way; later we’ll see how we can relax that restriction and use different orderings to make different queries more efficient.

Query Execution

The diagram below shows what a simple query plan for SELECT col_b FROM table WHERE col_a > 5 might look like for a query engine reading from a traditional row-major file format. A scan node reads every row in turn from disk, and streams the rows to a predicate evaluation node, which looks at the value of col_a in each row. Those rows that pass the predicate are sent to a projection node which constructs result tuples containing col_b.

Row Query Plan

Compare that to the query plan below, for a query engine reading from columnar storage. Each column referenced in the query is read independently. The predicate is evaluated over col_a to produce a list of matching row IDs. col_b is then scanned with respect to that list of IDs, and each matching value is returned as a query result. This query plan performs two IO seeks (to find the beginning of both column files), instead of one, and issues two consecutive reads rather than one large read. The pattern of using IDs for each column value is very common to make reconstructing rows easier; usually columns are all sorted on the same key so the Nth value of col_a belongs to the same row as the Nth value of col_b.

Columnar Query Plan

The extra IO cost for the row-format query is therefore the time it takes to read all those extra columns. Let’s assume the table is 10 columns wide, ten million rows long and each value is 4 bytes, which are all conservative estimates. Then there is an extra 8 * 1M * 4 bytes, or 32MB of extra data read, which is ~3.20s on a query that would likely otherwise take 800ms; an overhead of 300%. When disks are less performant, or column widths wider, the effect becomes exaggerated.

This, then, is the basic idea of columnar storage: we recognise that analytical workloads rarely require full scans of all table data, but do often require full scans of a small subset of the columns, and so we arrange to make column scans cheap at the expense of extra cost reading individual rows.

The Cost of Columnar

Is this a free lunch? Should every analytical database go out and change every file format to be column-major? Obviously the story is more complicated than that. There are some query archetypes that suffer when data is stored in a columnar format.

The obvious drawback is that it is expensive to reassemble a row, since the separate values that comprise it are spread far across the disk. Every column included in a projection implies an extra disk seek, and this can add up when the projectivity of a query is high. Therefore, for highly projective queries, row-major formats can be more efficient (and therefore columnar formats are not strictly better than row-major storage even from a pure IO perspective).

There are more subtle repurcussions of each row being scattered across the disk. When a row-major format is read into memory, and ultimately into CPU cache, it is in a format that permits cheap reference to multiple columns at a time. Row-major formats have good in-memory spatial locality, and there are common operations that benefit enormously from this.

For example, a query that selects the sum of two columns can sometimes be executed (once the data is in memory) faster on row-major formats, since the columns are almost always in the same cache line for each row. Columnar representations are less well suited; each column must be brought into memory at the same time and moved through in lockstep (yet this is still not cache efficient if each column is ordered differently), or the initial column must be scanned, each value buffered and then the second column scanned separately to complete the half-finished output tuple.

The same general problem arises when preparing each tuple to write out as a result of (non-aggregating) query. Selecting several columns at once requires ‘row reconstruction’ at some point in the query lifecycle. Deciding when to do this is a complicated process, and (as we shall see) the literature has not yet developed a good rule of thumb. Many databases are row-major internally, and therefore a columnar format is transposed into a row-major one relatively early in the scanning process. As described above, this can require buffering half-constructed tuples in memory. For this reason, columnar formats are often partiioned into ‘row-groups’; each column chunk N contains rows (K*N) to ((K+1) * N). This reduces the amount of buffering required, at the cost of a few more disk seeks.

Further Aspects of Columnar Storage

Fully column-oriented execution engines

Relevant papers:
C-Store: A Column-oriented DBMS
The Vertica Analytic Database: C-Store 7 Years Later
Materialization Strategies in a Column-Oriented DBMS
Performance Tradeoffs in Read-Optimized Databases
Column-Stores vs. Row-Stores: How Different Are They Really?

In this post, I’ve talked mostly about the benefits of columnar storage for scans – query operators that read data from disk, but whose ultimate output is a batch of rows for the rest of the query plan to operate on. In fact, columnar data can be integrated into pretty much every operator in a query execution engine. C-Store, the research project precursor to Vertica, explored a lot of the consequences of keeping data in columns until later on in the query plan. Eventually, of course, the columns have to be converted to rows, since the user expects a result in row-major format. The choice of when to perform this conversion is called late or early materialisation; viewed this way column-stores and row-stores can be considered two points on a spectrum of early to late materialisation strategies. Materialisation is studied in detail in the materialisation strategies paper above. Their conclusions are that the correct time to construct a tuple depends on the query plan (two broad patterns are considered: pipelining and parallel scans) and the query selectivity. Unfortunately, supporting both strategies would involve significant implementation cost – each operator would have to support two interfaces, and two parallel execution engines would effectively be frankensteined together. In general, late materialisation can lead to significant advantages: for example, by delaying the cost of reconstructing a tuple, it can be avoided if the tuple is ultimately filtered out by a predicate.

The difference between row-based and columnar execution engines is studied in the Performance Tradeoffs… and Column-Stores vs. Row-Stores… papers. The former takes a detailed look at when each strategy is superior – coming out in favour mostly of column-stores, but only with simple queries and basic query plans. The latter tries to implement common column-store optimisations in a traditional row-store, without changing the code. This means a number of increasingly brittle hacks to emulate columnar storage.

Compression

Relevant papers:
Integrating Compression and Execution on Column-Oriented Database Systems

A column of values drawn from the same set (like item price, say) is likely to be highly amenable to compression since the values contained are similar, and often identical. Compressing a column has at least two significant advantages on IO cost: less space is required on disk, and less IO required to bring a column into memory (at the cost of some CPU to decompress which is usually going spare). Some compression formats – for example run-length encoding – allow execution engines to operate on the compressed data directly, filtering large chunks at a time without first decompressing them. This is another advantage of late materialisation – by keeping the data compressed until late in the query plan, these optimisations become available to many operators, not just the scan.

Hybrid approaches

Relevant papers:
Weaving Relations for Cache Performance

Since neither row-major nor column-major is strictly superior on every workload, it’s natural that some research has been done into hybrid approaches that can achieve the best of both worlds. The most commonly known approach is PAX – Partition Attributes Across – which splits the table into page-sized groups of rows, and inside those groups formats the rows in column-major order. This is the same approach as the row-groups used to prevent excessive buffering described earlier, but this is not the aim of PAX; with PAX the original intention was to make CPU processing more efficient by having individual columns available contiguously to perform filtering, but also to have all the columns for a particular row nearby inside a group to make tuple reconstruction cheaper. The result of this approach is that IO costs don’t go down (because each row-group is only a page long, and is therefore read in its entirety), but reconstruction and filtering is cheaper than for true columnar formats.


출처 : http://www.smartdatacollective.com/kingmesal/386160/apache-drill-vs-apache-spark-what-s-right-tool-job


11
70
81

Image

If you’re looking to implement a big data project, you’re probably deciding whether to go with Apache Spark SQL or Apache Drill. This article can help you decide which query tool you should use for the kinds of projects you’re working on.

Spark SQL

Spark SQL is simply a module that lets you work with structured data using Apache Spark. It allows you to mix SQL within your existing Spark projects. Not only do you get access to a familiar SQL query language, you also get access to powerful tools such as Spark Streaming and the MLlib machine learning library.

Spark uses a special data structure called a DataFrame that represents data as named columns, similar to relational tables. You can query the data from Scala, Python, Java, and R. This enables you to perform powerful analysis of your data rather than just retrieving it. But it’s even more powerful when extracting data for use with the machine learning library. With MLlib, you can perform sophisticated analyses, detect credit card fraud, and process data coming from servers.

As with Drill, Spark SQL is compatible with a number of data formats, including some of the same ones that Drill supports: Parquet, JSON, and Hive. Spark SQL can handle multiple data sources similar to the way Drill can, but you can funnel the data into your machine learning systems mentioned earlier. This gives you a lot of power to analyze multiple data points, especially when combined with Spark Streaming. Spark SQL serves as a way to glue together different data sources and libraries into a powerful application.

Apache Drill

Apache Drill is a powerful database engine that also lets you use SQL for queries. You can use a number of data formats, including Parquet, MongoDB, MapR-DB, HDFS, MapR-FS, Amazon S3, Azure Blob Storage, Google Cloud Storage, Swift, NAS, and more.

You can use data from multiple data sources and join them without having to pull the data out, making Drill especially useful for business intelligence.

The ability to view multiple types of data, some of which have both strict and loose schema, as well as being able to allow for complex data models, might seem like a drag on performance. However, Drill uses schema discovery and a hierarchical columnar data model to treat data like a set of tables, independently of how the data is actually modeled. 

Almost all existing BI tools, including Tableau, Qlik, MicroStrategy, Spotfire, SAS, and even Excel, can use Drill’s JDBC and ODBC drivers to connect to it. This makes Drill very useful for people already using BI and SQL databases to move up to big data workloads using tools they’re already familiar with.

Drill’s JDBC driver lets BI tools access Drill. JDBC lets developers query large datasets using Java. This has a similar advantage that using ANSI SQL does: lots of developers are already familiar with Java and can transfer their skills to Drill.

Easy Data Access in Drill

One of Drill’s biggest strengths is its ability to secure databases at the file level using views and impersonation.

Views within Drill are the same as those within relational databases. They allow a simplified query to hide the complexities of the underlying tables. Impersonation allows a user to access data as another user. This enables fine-grained access to the raw data when other members of your team should not be able to view sensitive or secure data.

Views and impersonation are beyond the scope of Apache Spark.

Conclusion

So which query engine should you choose? As always, it depends. If you’re mainly looking to query data quickly, even across multiple data sources, then you should look into Drill. If you want to go beyond querying data and work with data in more algorithmic ways, then Spark SQL might be for you. You can always test both out by playing around in your own Sandbox environment, which lets you play around with these powerful systems on your own machine.



Authored by:

Jim Scott

James A. Scott (prefers to go by Jim) is Director, Enterprise Strategy & Architecture at MapR Technologies and is very active in the Hadoop community. Jim helped build the Hadoop community in Chicago as cofounder of the Chicago Hadoop Users Group. He has implemented Hadoop at three different companies, supporting a variety of enterprise use cases from managing Points of Interest for mapping ...

See complete profile


'빅데이터' 카테고리의 다른 글

Columnar Storage  (0) 2016.07.14
Hello, TensorFlow!  (0) 2016.07.08
분산 로그 수집기 Fluentd 소개  (0) 2016.06.14
람다 아키텍처(Lambda Architecture)  (0) 2016.05.18
Lambda Architecture  (0) 2016.05.18

출처 : https://www.oreilly.com/learning/hello-tensorflow


Hello, TensorFlow!

Building and training your first TensorFlow graph from the ground up.

The TensorFlow project is bigger than you might realize. The fact that it's a library for deep learning, and its connection to Google, has helped TensorFlow attract a lot of attention. But beyond the hype, there are unique elements to the project that are worthy of closer inspection:

  • The core library is suited to a broad family of machine learning techniques, not “just” deep learning.
  • Linear algebra and other internals are prominently exposed.
  • In addition to the core machine learning functionality, TensorFlow also includes its own logging system, its own interactive log visualizer, and even its own heavily engineered serving architecture.
  • The execution model for TensorFlow differs from Python's scikit-learn, or most tools in R.

Cool stuff, but—especially for someone hoping to explore machine learning for the first time—TensorFlow can be a lot to take in.

Get O'Reilly's weekly data newsletter

How does TensorFlow work? Let's break it down so we can see and understand every moving part. We'll explore the data flow graph that defines the computations your data will undergo, how to train models with gradient descent using TensorFlow, and how TensorBoard can visualize your TensorFlow work. The examples here won't solve industrial machine learning problems, but they'll help you understand the components underlying everything built with TensorFlow, including whatever you build next!

Names and execution in Python and TensorFlow

The way TensorFlow manages computation is not totally different from the way Python usually does. With both, it's important to remember, to paraphrase Hadley Wickham, that an object has no name (see Figure 1). In order to see the similarities (and differences) between how Python and TensorFlow work, let’s look at how they refer to objects and handle evaluation.

Names “have” objects, rather than the reverse
Figure 1. Names “have” objects, rather than the reverse. Image courtesy of Hadley Wickham, used with permission.

The variable names in Python code aren't what they represent; they're just pointing at objects. So, when you say in Python that foo = [] and bar = foo, it isn't just that foo equals bar; foo is bar, in the sense that they both point at the same list object.

>>> foo = []
>>> bar = foo
>>> foo == bar
## True
>>> foo is bar
## True

You can also see that id(foo) and id(bar) are the same. This identity, especially with mutable data structures like lists, can lead to surprising bugs when it's misunderstood.

Internally, Python manages all your objects and keeps track of your variable names and which objects they refer to. The TensorFlow graph represents another layer of this kind of management; as we’ll see, Python names will refer to objects that connect to more granular and managed TensorFlow graph operations.

When you enter a Python expression, for example at an interactive interpreter or Read Evaluate Print Loop (REPL), whatever is read is almost always evaluated right away. Python is eager to do what you tell it. So, if I tell Python to foo.append(bar), it appends right away, even if I never use foo again.

A lazier alternative would be to just remember that I said foo.append(bar), and if I ever evaluate foo at some point in the future, Python could do the append then. This would be closer to how TensorFlow behaves, where defining relationships is entirely separate from evaluating what the results are.

TensorFlow separates the definition of computations from their execution even further by having them happen in separate places: a graph defines the operations, but the operations only happen within a session. Graphs and sessions are created independently. A graph is like a blueprint, and a session is like a construction site.

Back to our plain Python example, recall that foo and bar refer to the same list. By appending bar into foo, we've put a list inside itself. You could think of this structure as a graph with one node, pointing to itself. Nesting lists is one way to represent a graph structure like a TensorFlow computation graph.

>>> foo.append(bar)
>>> foo
## [[...]]

Real TensorFlow graphs will be more interesting than this!

The simplest TensorFlow graph

To start getting our hands dirty, let’s create the simplest TensorFlow graph we can, from the ground up. TensorFlow is admirably easier to install than some other frameworks. The examples here work with either Python 2.7 or 3.3+, and the TensorFlow version used is 0.8.

>>> import tensorflow as tf

At this point TensorFlow has already started managing a lot of state for us. There's already an implicit default graph, for example. Internally, the default graph lives in the _default_graph_stack, but we don't have access to that directly. We use tf.get_default_graph().

>>> graph = tf.get_default_graph()

The nodes of the TensorFlow graph are called “operations,” or “ops.” We can see what operations are in the graph with graph.get_operations().

>>> graph.get_operations()
## []

Currently, there isn't anything in the graph. We’ll need to put everything we want TensorFlow to compute into that graph. Let's start with a simple constant input value of one.

>>> input_value = tf.constant(1.0)

That constant now lives as a node, an operation, in the graph. The Python variable name input_value refers indirectly to that operation, but we can also find the operation in the default graph.

>>> operations = graph.get_operations()
>>> operations
## [<tensorflow.python.framework.ops.Operation at 0x1185005d0>]
>>> operations[0].node_def
## name: "Const"
## op: "Const"
## attr {
##   key: "dtype"
##   value {
##     type: DT_FLOAT
##   }
## }
## attr {
##   key: "value"
##   value {
##     tensor {
##       dtype: DT_FLOAT
##       tensor_shape {
##       }
##       float_val: 1.0
##     }
##   }
## }

TensorFlow uses protocol buffers internally. (Protocol buffers are sort of like a Google-strength JSON.) Printing the node_def for the constant operation above shows what's in TensorFlow's protocol buffer representation for the number one.

People new to TensorFlow sometimes wonder why there's all this fuss about making “TensorFlow versions” of things. Why can't we just use a normal Python variable without also defining a TensorFlow object? One of the TensorFlow tutorials has an explanation:

To do efficient numerical computing in Python, we typically use libraries like NumPy that do expensive operations such as matrix multiplication outside Python, using highly efficient code implemented in another language. Unfortunately, there can still be a lot of overhead from switching back to Python every operation. This overhead is especially bad if you want to run computations on GPUs or in a distributed manner, where there can be a high cost to transferring data.

TensorFlow also does its heavy lifting outside Python, but it takes things a step further to avoid this overhead. Instead of running a single expensive operation independently from Python, TensorFlow lets us describe a graph of interacting operations that run entirely outside Python. This approach is similar to that used in Theano or Torch.

TensorFlow can do a lot of great things, but it can only work with what's been explicitly given to it. This is true even for a single constant.

If we inspect our input_value, we see it is a constant 32-bit float tensor of no dimension: just one number.

>>> input_value
## <tf.Tensor 'Const:0' shape=() dtype=float32>

Note that this doesn't tell us what that number is. To evaluate input_value and get a numerical value out, we need to create a “session” where graph operations can be evaluated and then explicitly ask to evaluate or “run” input_value. (The session picks up the default graph by default.)

>>> sess = tf.Session()
>>> sess.run(input_value)
## 1.0

It may feel a little strange to “run” a constant. But it isn't so different from evaluating an expression as usual in Python; it's just that TensorFlow is managing its own space of things—the computational graph—and it has its own method of evaluation.

The simplest TensorFlow neuron

Now that we have a session with a simple graph, let's build a neuron with just one parameter, or weight. Often, even simple neurons also have a bias term and a non-identity activation function, but we'll leave these out.

The neuron's weight isn't going to be constant; we expect it to change in order to learn based on the “true” input and output we use for training. The weight will be a TensorFlow variable. We'll give that variable a starting value of 0.8.

>>> weight = tf.Variable(0.8)

You might expect that adding a variable would add one operation to the graph, but in fact that one line adds four operations. We can check all the operation names:

>>> for op in graph.get_operations(): print(op.name)
## Const
## Variable/initial_value
## Variable
## Variable/Assign
## Variable/read

We won't want to follow every operation individually for long, but it will be nice to see at least one that feels like a real computation.

>>> output_value = weight * input_value

Now there are six operations in the graph, and the last one is that multiplication.

>>> op = graph.get_operations()[-1]
>>> op.name
## 'mul'
>>> for op_input in op.inputs: print(op_input)
## Tensor("Variable/read:0", shape=(), dtype=float32)
## Tensor("Const:0", shape=(), dtype=float32)

This shows how the multiplication operation tracks where its inputs come from: they come from other operations in the graph. To understand a whole graph, following references this way quickly becomes tedious for humans. TensorBoard graph visualization is designed to help.

How do we find out what the product is? We have to “run” the output_value operation. But that operation depends on a variable: weight. We told TensorFlow that the initial value of weight should be 0.8, but the value hasn't yet been set in the current session. The tf.initialize_all_variables() function generates an operation which will initialize all our variables (in this case just one) and then we can run that operation.

>>> init = tf.initialize_all_variables()
>>> sess.run(init)

The result of tf.initialize_all_variables() will include initializers for all the variables currently in the graph, so if you add more variables you'll want to use tf.initialize_all_variables() again; a stale init wouldn't include the new variables.

Now we're ready to run the output_value operation.

>>> sess.run(output_value)
## 0.80000001

Recall that's 0.8 * 1.0 with 32-bit floats, and 32-bit floats have a hard time with 0.8; 0.80000001 is as close as they can get.

See your graph in TensorBoard

Up to this point, the graph has been simple, but it would already be nice to see it represented in a diagram. We'll use TensorBoard to generate that diagram. TensorBoard reads the name field that is stored inside each operation (quite distinct from Python variable names). We can use these TensorFlow names and switch to more conventional Python variable names. Using tf.mul here is equivalent to our earlier use of just * for multiplication, but it lets us set the name for the operation.

>>> x = tf.constant(1.0, name='input')
>>> w = tf.Variable(0.8, name='weight')
>>> y = tf.mul(w, x, name='output')

TensorBoard works by looking at a directory of output created from TensorFlow sessions. We can write this output with a SummaryWriter, and if we do nothing aside from creating one with a graph, it will just write out that graph.

The first argument when creating the SummaryWriter is an output directory name, which will be created if it doesn't exist.

>>> summary_writer = tf.train.SummaryWriter('log_simple_graph', sess.graph)

Now, at the command line, we can start up TensorBoard.

$ tensorboard --logdir=log_simple_graph

TensorBoard runs as a local web app, on port 6006. (“6006” is “goog” upside-down.) If you go in a browser to localhost:6006/#graphs you should see a diagram of the graph you created in TensorFlow, which looks something like Figure 2.

A TensorBoard visualization of the simplest TensorFlow neuron
Figure 2. A TensorBoard visualization of the simplest TensorFlow neuron.

Making the neuron learn

Now that we’ve built our neuron, how does it learn? We set up an input value of 1.0. Let's say the correct output value is zero. That is, we have a very simple “training set” of just one example with one feature, which has the value one, and one label, which is zero. We want the neuron to learn the function taking one to zero.

Currently, the system takes the input one and returns 0.8, which is not correct. We need a way to measure how wrong the system is. We'll call that measure of wrongness the “loss” and give our system the goal of minimizing the loss. If the loss can be negative, then minimizing it could be silly, so let's make the loss the square of the difference between the current output and the desired output.

>>> y_ = tf.constant(0.0)
>>> loss = (y - y_)**2

So far, nothing in the graph does any learning. For that, we need an optimizer. We'll use a gradient descent optimizer so that we can update the weight based on the derivative of the loss. The optimizer takes a learning rate to moderate the size of the updates, which we'll set at 0.025.

>>> optim = tf.train.GradientDescentOptimizer(learning_rate=0.025)

The optimizer is remarkably clever. It can automatically work out and apply the appropriate gradients through a whole network, carrying out the backward step for learning.

Let's see what the gradient looks like for our simple example.

>>> grads_and_vars = optim.compute_gradients(loss)
>>> sess.run(tf.initialize_all_variables())
>>> sess.run(grads_and_vars[1][0])
## 1.6

Why is the value of the gradient 1.6? Our loss is error squared, and the derivative of that is two times the error. Currently the system says 0.8 instead of 0, so the error is 0.8, and two times 0.8 is 1.6. It's working!

For more complex systems, it will be very nice indeed that TensorFlow calculates and then applies these gradients for us automatically.

Let's apply the gradient, finishing the backpropagation.

>>> sess.run(optim.apply_gradients(grads_and_vars))
>>> sess.run(w)
## 0.75999999  # about 0.76

The weight decreased by 0.04 because the optimizer subtracted the gradient times the learning rate, 1.6 * 0.025, pushing the weight in the right direction.

Instead of hand-holding the optimizer like this, we can make one operation that calculates and applies the gradients: the train_step.

>>> train_step = tf.train.GradientDescentOptimizer(0.025).minimize(loss)
>>> for i in range(100):
>>>     sess.run(train_step)
>>> 
>>> sess.run(y)
## 0.0044996012

Running the training step many times, the weight and the output value are now very close to zero. The neuron has learned!

Training diagnostics in TensorBoard

We may be interested in what's happening during training. Say we want to follow what our system is predicting at every training step. We could print from inside the training loop.

>>> sess.run(tf.initialize_all_variables())
>>> for i in range(100):
>>>     print('before step {}, y is {}'.format(i, sess.run(y)))
>>>     sess.run(train_step)
>>> 
## before step 0, y is 0.800000011921
## before step 1, y is 0.759999990463
## ...
## before step 98, y is 0.00524811353534
## before step 99, y is 0.00498570781201

This works, but there are some problems. It's hard to understand a list of numbers. A plot would be better. And even with only one value to monitor, there's too much output to read. We're likely to want to monitor many things. It would be nice to record everything in some organized way.

Luckily, the same system that we used earlier to visualize the graph also has just the mechanisms we need.

We instrument the computation graph by adding operations that summarize its state. Here, we'll create an operation that reports the current value of y, the neuron's current output.

>>> summary_y = tf.scalar_summary('output', y)

When you run a summary operation, it returns a string of protocol buffer text that can be written to a log directory with a SummaryWriter.

>>> summary_writer = tf.train.SummaryWriter('log_simple_stats')
>>> sess.run(tf.initialize_all_variables())
>>> for i in range(100):
>>>     summary_str = sess.run(summary_y)
>>>     summary_writer.add_summary(summary_str, i)
>>>     sess.run(train_step)
>>> 

Now after running tensorboard --logdir=log_simple_stats, you get an interactive plot at localhost:6006/#events (Figure 3).

A TensorBoard visualization of a neuron’s output against training iteration number
Figure 3. A TensorBoard visualization of a neuron’s output against training iteration number.

Flowing onward

Here's a final version of the code. It's fairly minimal, with every part showing useful (and understandable) TensorFlow functionality.

import tensorflow as tf

x = tf.constant(1.0, name='input')
w = tf.Variable(0.8, name='weight')
y = tf.mul(w, x, name='output')
y_ = tf.constant(0.0, name='correct_value')
loss = tf.pow(y - y_, 2, name='loss')
train_step = tf.train.GradientDescentOptimizer(0.025).minimize(loss)

for value in [x, w, y, y_, loss]:
    tf.scalar_summary(value.op.name, value)

summaries = tf.merge_all_summaries()

sess = tf.Session()
summary_writer = tf.train.SummaryWriter('log_simple_stats', sess.graph)

sess.run(tf.initialize_all_variables())
for i in range(100):
    summary_writer.add_summary(sess.run(summaries), i)
    sess.run(train_step)

The example we just ran through is even simpler than the ones that inspired it in Michael Nielsen's Neural Networks and Deep Learning. For myself, seeing details like these helps with understanding and building more complex systems that use and extend from simple building blocks. Part of the beauty of TensorFlow is how flexibly you can build complex systems from simpler components.

If you want to continue experimenting with TensorFlow, it might be fun to start making more interesting neurons, perhaps with different activation functions. You could train with more interesting data. You could add more neurons. You could add more layers. You could dive into more complex pre-built models, or spend more time with TensorFlow's own tutorials and how-to guides. Go for it!


Article image: Braided river. (source: National Park Service, Alaska Region on Flickr).

Aaron Schumacher

Aaron Schumacher is a data scientist and software engineer for Deep Learning Analytics. He has taught with Python and R for General Assembly and the Metis data science bootcamp. Aaron has also worked with data at Booz Allen Hamilton, New York University, and the New York City Department of Education. Aaron’s career-best breakdancing result was advancing to the semi-finals of the R16 Korea 2009 individual footwork battle. He is honored to now be the least significant contributor to TensorFlow 0.9. 



Neural Network Introduction

들어가며

최근 Machine Learning 분야에서 가장 뜨거운 분야는 누가 뭐래도 Deep Learning이다. 엄청나게 많은 사람들이 관심을 가지고 있고, 공부하고 응용하고 있지만, 체계적으로 공부할 수 있는 자료가 많이 없다는 것이 개인적으로 조금 안타깝다. 이제 막 각광받기 시작한지 10년 정도 지났고, 매년 새로운 자료들이 쏟아져나오기 때문에 책이나 정리된 글을 찾기가 쉽지가 않다. 그러나 Deep Learning은 결국 artificial neural network를 조금 더 복잡하게 만들어놓은 모델이고, 기본적인 neural network에 대한 이해만 뒷받침된다면 자세한 내용들은 천천히 탑을 쌓는 것이 가능하다고 생각한다. 이 글에서는 neural network의 가장 기본적인 model에 대해 다루고, model paramter를 update하는 algorithm인 backpropagation에 대해서 다룰 것이다. 조금 더 advanced한 topic들은 이 다음 글에서 다룰 예정이다. 이 글의 일부 문단은 이전 글들을 참고하였다.

Motivation of Neural Network

이름에서부터 알 수 있듯 neural network는 사람의 뇌를 본 따서 만든 머신러닝 모델이다 (참고: 원래 neural network의 full name은 artificial neural network이지만, 일반적으로 neural network라고 줄여서 부른다). 본격적으로 neural network에 대해 설명을 시작하기 전에 먼저 인간보다 컴퓨터가 훨씬 잘 할 수 있는 일들이 무엇이 있을지 생각해보자.

  • 1부터 10000000까지 숫자 더하기
  • 19312812931이 소수인지 아닌지 판별하기
  • 주어진 10000 by 10000 matrix 의 determinant값 계산하기
  • 800 페이지 짜리 책에서 ‘컴퓨터’ 라는 단어가 몇 번 나오는지 세기

반면 인간이 컴퓨터보다 훨씬 잘 할 수 있는 일들에 대해 생각해보자

  • 다른 사람과 상대방이 말하고자하는 바를 완벽하게 이해하면서 내가 하고 싶은 말을 상대도 이해할 수 있도록 전달하기
  • 주어진 사진이 고양이 사진인지 강아지 사진인지 판별하기
  • 사진으로 찍어보낸 문서 읽고 이해하기
  • 주어진 사진에서 얼마나 많은 물체가 있는지 세고, 사진에 직접 표시하기

컴퓨터가 잘 할 수 있는 0과 1로 이루어진 사칙연산이다. 기술의 발달로 인해 지금은 컴퓨터가 예전보다도 더 빠른 시간에, 그리고 더 적은 전력으로 훨씬 더 많은 사칙연산을 처리할 수 있다. 반면 사람은 사칙연산을 컴퓨터만큼 빠르게 할 수 없다. 인간의 뇌는 오직 빠른 사칙연산만을 처리하기 위해 만들어진 것이 아니기 때문이다. 그러나 인지, 자연어처리 등의 그 이상의 무언가를 처리하기 위해서는 사칙연산 그 너머의 것들을 할 수 있어야하지만 현재 컴퓨터로는 인간의 뇌가 할 수 있는 수준으로 그런 것들을 처리할 수 없다.

예를 들어 아래와 같이 주어진 사진에서 각각의 물체를 찾아내는 문제를 생각해보자 (출처: 링크). 사람에게는 너무나 간단한 일이지만, 컴퓨터가 처리하기에는 너무나 어려운 일이다. 어떻게 어디부터 어디까지가 ‘tv or monitor’라고 판단할 수 있을까? 컴퓨터에게 사진은 단순한 0과 1로 이루어진 픽셀 데이터에 지나지 않기 때문에 이는 아주 어려운 일이다.

그렇기 때문에 자연언어처리, 컴퓨터 비전 등의 영역에서는 인간과 비슷한 성능을 내는 시스템을 만들 수만 있다면 엄청난 기술적 진보가 일어날 수 있을 것이다. 그렇기 때문에 인간의 능력을 쫓아가는 것 이전에, 먼저 인간의 뇌를 모방해보자라는 아이디어를 낼 수 있을 것이다. Neural Network는 이런 모티베이션으로 만들어진 간단한 수학적 모델이다. 우리는 이미 인간의 뇌가 엄청나게 많은 뉴런들과 그것들을 연결하는 시냅스로 구성되어있다는 사실을 알고 있다. 또한 각각의 뉴런들이 activate되는 방식에 따라서 다른 뉴런들도 activate 되거나 activate되지 않거나 하는 등의 action을 취하게 될 것이다. 그렇다면 이 사실들을 기반으로 다음과 같은 간단한 수학적 모델을 정의하는 것이 가능하다.

Model of Neural Network: neuron, synapse, activation function

먼저 뉴런들이 node이고, 그 뉴런들을 연결하는 시냅스가 edge인 네트워크를 만드는 것이 가능하다. 각각의 시냅스의 중요도가 다를 수 있으므로 edge마다 weight를 따로 정의하게 되면 아래 그림과 같은 형태로 네트워크를 만들 수 있다. (출처: 위키)

보통 neural network는 directed graph이다. 즉, information propagation이 한 방향으로 고정된다는 뜻이다. 만약 undirected edge를 가지게 되면, 혹은 동일한 directed edge가 양방향으로 주어질 경우, information propagation이 recursive하게 일어나서 결과가 조금 복잡해진다. 이런 경우를 recurrent neural network (RNN)이라고 하는데, 과거 데이터를 저장하는 효과가 있기 때문에 최근 음성인식 등의 sequencial data를 처리할 때 많이 사용되고 있다. 이번 ICML 2015에서도 RNN 논문이 많이 발표되고 있고, 최근들어 연구가 활발한 분야이다. 이 글에서는 일단 가장 간단한 ‘multi layer perceptron (MLP)’라는 구조만 다룰 것인데, 이 구조는 directed simple graph이고, 같은 layer들 안에서는 서로 connection이 없다. 즉, self-loop와 parallel edge가 없고, layer와 layer 사이에만 edge가 존재하며, 서로 인접한 layer끼리만 edge를 가진다. 즉, 첫번째 layer와 네번째 layer를 직접 연결하는 edge가 없는 것이다. 앞으로 layer에 대한 특별한 언급이 없다면 이런 MLP라고 생각하면 된다. 참고로 이 경우 information progation이 ‘forward’로만 일어나기 때문에 이런 네트워크를 feed-forward network라고 부르기도 한다.

다시 일반적인 neural network에 대해 생각해보자. 실제 뇌에서는 각기 다른 뉴런들이 activate되고, 그 결과가 다음 뉴런으로 전달되고 또 그 결과가 전달되면서 최종 결정을 내리는 뉴런이 activate되는 방식에 따라 정보를 처리하게 된다. 이 방식을 수학적 모델로 바꿔서 생각해보면, input 데이터들에 대한 activation 조건을 function으로 표현하는 것이 가능할 것이다. 이것을 activate function이라고 정의한다. 가장 간단한 activation function의 예시는 들어오는 모든 input 값을 더한 다음, threshold를 설정하여 이 값이 특정 값을 넘으면 activate, 그 값을 넘지 못하면 deactivate되도록 하는 함수일 것이다. 일반적으로 많이 사용되는 여러 종류의 activate function이 존재하는데, 몇 가지를 소개해보도록 하겠다. 편의상 t=iwixi라고 정의하겠다. (참고로, 일반적으로는 weight 뿐 아니라 bais도 고려해야한다. 이 경우 t=i(wixi+bi)로 표현이 되지만, 이 글에서는 bais는 weight와 거의 동일하기 때문에 무시하고 진행하도록 하겠다. - 예를 들어 항상 값이 1인 x0를 추가한다면 w0가 bais가 되므로, 가상의 input을 가정하고 weight와 bais를 동일하게 취급하여도 무방하다.)

  • sigmoid function: f(t)=11+et

  • tanh function: f(t)=etetet+et

  • absolute function: f(t)=t

  • ReLU function: f(t)=max(0,t)

보통 가장 많이 예시로 드는 activation function으로 sigmoid function이 있다. (출처는 위의 위키와 같음)

이 함수는 미분이 간단하다거나, 실제 뉴런들이 동작하는 것과 비슷하게 생겼다는 등의 이유로 과거에는 많이 사용되었지만, 별로 practical한 activation function은 아니고, 실제로는 ReLU를 가장 많이 사용한다 (2012년 ImageNet competition에서 우승했던 AlexNet publication을 보면, ReLU와 dropout을 쓰는 것이 그렇지 않은 것보다 훨씬 더 우수한 결과를 얻는다고 주장하고 있다. 이에 대한 자세한 내용은 다른 포스트를 통해 보충하도록 하겠다). 참고로 neuron을 non-linearity라고 부르기도 하는데, 그 이유는 activation function으로 linear function을 사용하게 되면 아무리 여러 neuron layer를 쌓는다고 하더라도 그것이 결국 하나의 layer로 표현이 되기 때문에 non-linear한 activation function을 사용하기 때문이다.

따라서 이 모델은 처음에 node와 edge로 이루어진 네트워크의 모양을 정의하고, 각 node 별 activation function을 정의한다. 이렇게 정해진 모델을 조절하는 parameter의 역할은 edge의 weight가 맡게되며, 가장 적절한 weight를 찾는 것이 이 수학적 모델을 train할 때의 목표가 될 것이다.

Inference via Neural Network

먼저 모든 paramter가 결정되었다고 가정하고 neural network가 어떻게 결과를 inference하는지 살펴보도록하자. Neural network는 먼저 주어진 input에 대해 다음 layer의 activation을 결정하고, 그것을 사용해 그 다음 layer의 activation을 결정한다. 이런 식으로 맨 마지막까지 결정을 하고 나서, 맨 마지막 decision layer의 결과를 보고 inference를 결정하는 것이다 (아래 그림 참고, 빨간 색이 activate된 뉴런이다).

이때, classification이라고 한다면 마지막 layer에 내가 classification하고 싶은 class 개수만큼 decision node를 만든 다음 그 중 하나 activate되는 값을 선택하는 것이다. 예를 들어 0부터 9까지 손글씨 데이터를 (MNIST라는 유명한 dataset이 있다) classification해야한다고 생각해보자. 그 경우는 0부터 9까지 decision이 총 10개이므로 마지막 decision layer에는 10개의 neuron이 존재하게 되고 주어진 데이터에 대해 가장 activation된 크기가 큰 decision을 선택하는 것이다.

Backpropagation Algorithm

마지막으로 이제 weight를 어떻게 찾을 수 있는지 weight paramter를 찾는 알고리즘에 대해 알아보자. 먼저 한 가지 알아두어야 할 점은 activation function들이 non-linear하고, 이것들이 서로 layer를 이루면서 복잡하게 얽혀있기 때문에 neural network의 weight optimization이 non-convex optimization이라는 것이다. 따라서 일반적인 경우에 neural network의 paramter들의 global optimum을 찾는 것은 불가능하다. 그렇기 때문에 보통 gradient descent 방법을 사용하여 적당한 값까지 수렴시키는 방법을 사용하게 된다.

Neural network (이 글에서는 multi-layer feed-forward network)의 parameter를 update하기 위해서는 backpropagation algorithm이라는 것을 주로 사용하는데, 이는 단순히 neural network에서 gradient descent를 chain rule을 사용하여 단순화시킨 것에 지나지 않는다 (Gradient descent에 대해서는 이전에 쓴 Convex Optimization글에서 자세히 다루고 있으니 참고하면 좋을 것 같다). 모든 optimization 문제는 target function이 정의되어야 풀 수 있다. Neural network에서는 마지막 decision layer에서 우리가 실제로 원하는 target output과 현재 network가 produce한 estimated output끼리의 loss function을 계산하여 그 값을 minimize하는 방식을 취한다. 일반적으로 많이 선택하는 loss에는 다음과 같은 함수들이 있다. 이때 우리가 원하는 d-dimensional target output을 t=[t1,,td]로, estimated output을 x=[x1,,xd] 로 정의해보자.

  • sum of squares (Euclidean) loss: i=1d(xiti)2

  • softmax loss: i=1d[tilog(exij=1dexj)+(1ti)log(1exij=1dexj)]

  • cross entropy loss: i=1d[tilogxi(1ti)log(1xi)]

  • hinge loss: max(0,1tx), 이때 은 내적을 의미한다.

상황에 따라 조금씩 다른 loss function을 사용하지만, classification에 대해서는 보통 softmax loss가 gradient의 값이 numerically stable하기 때문에 softmax loss를 많이 사용한다. 이렇게 loss function이 주어진다면, 이 값을 주어진 paramter들에 대해 gradient를 구한 다음 그 값들을 사용해 parameter를 update하기만 하면 된다. 문제는, 일반적인 경우에 대해 이 paramter 계산이 엄청 쉬운 것만은 아니라는 것이다.

Backpropagtaion algorithm은 chain rule을 사용해 gradient 계산을 엄청 간단하게 만들어주는 알고리즘으로, 각각의 paramter의 grdient를 계산할 때 parallelization도 용이하고, 알고리즘 디자인만 조금 잘하면 memory도 많이 아낄 수 있기 때문에 실제 neural network update는 이 backpropagtaion 알고리즘을 사용하게 된다.

Gradient descent method를 사용하기 위해서는 현재 parameter에 대한 gradient를 계산해야하지만, 네트워크가 복잡해지면 그 값을 바로 계산하는 것이 엄청나게 어려워진다. 그 대신 backpropataion algorithm에서는 먼저 현재 paramter를 사용하여 loss를 계산하고, 각각의 parameter들이 해당 loss에 대해 얼마만큼의 영향을 미쳤는지 chain rule을 사용하여 계산하고, 그 값으로 update를 하는 방법이다. 따라서 backpropagation algorithm은 크게 두 가지 phase로 나눌 수가 있는데, 하나는 propagation phase이며, 하나는 weight update phase이다. propagation phase에서는 training input pattern에서부터 에러, 혹은 각 뉴런들의 변화량을 계산하며, weight update phase에서는 앞에서 계산한 값을 사용해 weight를 update시킨다.

Phase 1: Propagation
  1. Forward propagation: input training data로부터 output을 계산하고, 각 ouput neuron에서의 error를 계산한다. (input -> hidden -> output 으로 정보가 흘러가므로 ‘forward’ propagation이라 한다.)
  2. Back propagation: output neuron에서 계산된 error를 각 edge들의 weight를 사용해 바로 이전 layer의 neuron들이 얼마나 error에 영향을 미쳤는지 계산한다. (output -> hidden 으로 정보가 흘러가므로 ‘back’ propagation이라 한다.)
Phase 2: Weight update
  1. Chain rule을 사용해 paramter들의 gradient를 계산한다.

이때, chain rule을 사용한다는 의미는 아래 그림에서 나타내는 것처럼, 앞에서 계산된 gradient를 사용해 지금 gradient 값을 update한다는 의미이다. (그림은 bengio의 deep learning book Ch6 에서 가져왔다.)

두 그림 모두 zx를 구하는 것이 목적인데, 직접 그 값을 계산하는 대신, y layer에서 이미 계산한 derivative인 zy와 y layer와 x에만 관계있는 yx를 사용하여 원하는 값을 계산하고 있다. 만약 x 아래에 x이라는 parameter가 또 있다면, zx와 xx을 사용하여 zx을 계산할 수 있는 것이다. 때문에 우리가 backpropagation algorithm에서 필요한 것은 내가 지금 update하려는 paramter의 바로 전 variable의 derivative와, 지금 paramter로 바로 전 variable을 미분한 값 두 개 뿐이다. 이 과정을 output layer에서부터 하나하나 내려오면서 반복된다. 즉, output -> hidden k, hidden k -> hidden k-1, … hidden 2 -> hidden 1, hidden 1 -> input의 과정을 거치면서 계속 weight가 update되는 것이다. 예를 들어서 decision layer와 가장 가까운 weight는 직접 derivative를 계산하여 구할 수 있고, 그보다 더 아래에 있는 layer의 weight는 그 바로 전 layer의 weight와 해당 layer의 activation function의 미분 값을 곱하여 계산할 수 있다. 이해가 조금 어렵다면 아래의 예제를 천천히 읽어보기를 권한다.

이 과정을 맨 위에서 아래까지 반복하면 전체 gradient를 구할 수 있고, 이 gradient를 사용해 parameter들을 update할 수 있다. 이렇게 한 번의 iteration이 진행되고, 충분히 converge했다고 판단할 때 까지 이런 iteration을 계속 반복하는 것이 feed-forward network의 parameter를 update하는 방법이다.

이를 그림으로 표현하면 아래와 같다. (출처: 링크)

이렇듯 backpropagation은 직접 weight를 바로 변화시키는 것이 아니라 오직 error만을 보고 gradient descent method based approach를 사용해 error를 minimize하는 방향으로 계속 weight를 update시키는 것이다. 또한 한 번 error가 연산된 이후에는 output layer에서부터 그 이전 layer로 ‘역으로’ 정보가 update되기 때문에 이를 backpropagation, 한국어로는 역전사라고 하는 것이다.

Stochastic Gradient Descent

Gradient를 계산했으니 이제 직접 Gradient Descent를 써서 parameter만 update하면 된다. 그러나 문제가 하나 있는데, 일반적으로 neural network의 input data의 개수가 엄청나게 많다는 것이다. 때문에 정확한 gradient를 계산하기 위해서는 모든 training data에 대해 gradient를 전부 계산하고, 그 값을 평균 내어 정확한 gradient를 구한 다음 ‘한 번’ update해야한다. 그러나 이런 방법은 너무나도 비효율적이기 때문에 Stochastic Gradient Descent (SGD) 라는 방법을 사용해야한다.

SGD는 모든 데이터의 gradient를 평균내어 gradient update를 하는 대신 (이를 ‘full batch’라고 한다), 일부의 데이터로 ‘mini batch’를 형성하여 한 batch에 대한 gradient만을 계산하여 전체 parameter를 update한다. Convex optimization의 경우, 특정 조건이 충족되면 SGD와 GD가 같은 global optimum으로 수렴하는 것이 증명되어있지만, neural network는 convex가 아니기 때문에 batch를 설정하는 방법에 따라 수렴하는 조건이 바뀌게 된다. Batch size는 일반적으로 메모리가 감당할 수 있을 정도까지 최대한 크게 잡는 것 같다.

Backpropagation Algorithm: example

이전에 chain rule로 gradient를 계산한다고 언급했었는데, 실제 이 chain rule이 어떻게 적용되는지 아래의 간단한 예를 통해 살펴보도록하자. 이때 계산의 편의를 위해 각각의 neuron은 sigmoid loss를 가지고 있다고 가정하도록 하겠다.

이때 각각의 neuron의 input으로 들어가는 값을 ino5, output으로 나가는 값을 outh3와 같은 식으로 정의해보자 (이렇게 된다면 in과 out은 outh3=σ(inh3) 으로 표현 가능하다. - 이때 σ는 sigmoid function). 먼저 error를 정의하자. error는 가장 간단한 sum of square loss를 취하도록 하겠다. 우리가 원하는 target을 t라고 정의하면 loss는 E=12(t5outo5)2+12(t6outo6)2가 될 것이다 (1/2는 미분한 값을 깔끔하게 쓰기 위해 붙인 상관없는 값이므로 무시해도 좋다). 그리고 우리가 원하는 값들은 Ew13,Ew14,,Ew46이 될 것이다. 이제 가장 먼저 Ew35 부터 계산해보자.

Ew35=Eouto5outo5ino5ino5w35.

즉, 우리가 원하는 derivative를 계산하기 위해서는 세 개의 다른 derivative (Eouto5,outo5ino5,ino5w35)를 계산해야한다. 각각을 구하는 방법은 다음과 같다.

  • Eouto5: error를 E=12(t5outo5)2+12(t6outo6)2라고 정의했으므로, Eouto5=outo5t5이다. - 이때 outo5와 t5는 weight update이전 propagation step에서 계산된 값이다.

  • outo5ino5o5는 sigmoid activation function을 사용하므로 outo5=σ(ino5)이다. 또한 sigmoid function의 미분 값은 σ(x)x=σ(x)(1σ(x))으로 주어지므로, 이 값을 대입하면 outo5ino5=outo5(1outo5)가 된다. - 역시 여기에서도 미리 계산한 outo5를 사용한다.

  • ino5w35o5로 들어온 값의 총 합은 앞선 layer의 output과 o5로 들어오는 weight를 곱하면 되므로 ino5=w35outh3+w45outh4이고, 이것을 통해 ino5w35=outh3가 됨을 알 수 있다. - outh3 역시 이전 propagation에서 계산된 값이다.

따라서 Ew35의 derivative 값은 위의 세 값을 모두 곱한 것으로 계산 할 수 있다. 그림으로 표현하면 아래와 같은 그림이 될 것이다. 즉, ‘backward’ 방향으로 derivative에 대한 정보를 ‘propagation’하면서 parameter의 derivative를 계산하는 것이다. 마찬가지 방법으로 w36,w45,w46에 대한 derivative도 계산할 수 있다.

그럼 이번에는 그 전 layer의 paramter들 중 하나인 w13의 derivative를 계산해보자. 이번에 계산할 과정도 위와 비슷한 그림으로 표현해보면 아래와 같다.

그러면 이제 Ew13을 구해보자.

Ew13=Eouth3outh3inh3inh3w13.

마찬가지로 각각을 구하는 방법에 대해 적어보자.

  • Eouth3E=12(t5outo5)2+12(t6outo6)2를 E=Eo5+Eo6로 decompose 하면 이 미분 식은 Eo5outh3+Eo6outh3로 쓸 수 있다. 각각의 계산은 다음과 같다.

    • Eo5outh3=Eo5ino5ino5outh3으로 쓸 수 있다. 이 중 앞의 값인 Eo5ino5은 이미 전 과정에서 계산했던 Eouto5과 outo5ino5의 곱으로 계산가능하다. 뒤의 값은 ino5outh3=w35이므로 간단하게 계산할 수 있다.

    • Eo6outh3도 위와 같은 방법으로 연산이 가능하다.

  • outh3inh3outo5ino5와 같다. 따라서 outh3(1outh3)이다.

  • inh3w13ino5w35와 같다. 따라서 outi1이다.

이렇게 Eouth3에서는 앞에서 계산했던 값들을 재활용하고, 아래의 값들은 activation function과 network의 topological property에 맞는 derivative를 곱하는 방식으로 Ew13을 구할 수 있다.

이렇듯 backpropagation algorithm은 forward propagation을 통해 필요한 값들을 미리 저장해두고, backward propagation이 진행되면서 위에서부터 loss에 대한 derivative를 하나하나 계산해나가면서 다음 layer에서 바로 전 layer에서 계산한 값들과 각 neuron 별로 추가적으로 필요한 derivative들을 곱해나가면서 weight의 derivative를 계산하는 알고리즘이다.

이렇게 한 번 전체 gradient를 계산한 다음에는 learning rate를 곱하여 전체 parameter의 값을 update한 다음, 다시 처음부터 이 과정을 반복한다. 보통 에러가 감소하는 속도를 관측하면서 ‘이 정도면 converge한 것 같다’ 하는 수준까지 돌린다.

익숙해지려면 다소 시간이 걸리지만, 개념적으로 먼저 ‘error를 먼저 계산하고, 그 값을 아래로 전달해나가면서 바로 전 layer에서 계산한 미분값들을 사용해 현재 layer의 미분값을 계산한 다음, 그 값을 사용해 다음 layer의 미분값을 계산한다.’ 라고 개념만 이해해두고 다시 차근차근 chain rule을 계산해나가면서 계산하면 조금 편하게 익숙해 질 수 있을 것이다.

Backpropagation Algorithm: In Practice

실제 backpropagtion을 계산해야한다고 가정해보자. 편의상 l번째 hidden layer를 yl 이라고 해보자. 이 경우 각 layer에 대해 backpropagation algorithm을 위해 계산해야할 것은 총 두 가지 이다. Loss를 E라고 적었을 때 먼저 layer l의 parameter θl의 gradient인 Ewl을 구해야한다. 이 값은 Ewl=Eylylwl을 통해 계산한다. 이때, Eyl=Eyl+1yl+1yl이므로 Eyl은 바로 전 layer에서 넘겨준 Eyl+1의 값을 사용하여 계산하게 된다. 정리하면 실제 계산해야하는 값은 yl+1yl,ylwl 두 가지이고, 이 값들을 사용해 Eyl,Ewl을 return하게 된다. 앞의 값은 다음 layer에 넘겨줘서 다음 input으로 사용하고, 두 번째 값은 저장해두었다가 gradient descent update할 때 사용한다.

두 가지 예를 들어보자. 먼저 Inner Product layer 혹은 fully connected layer이다. 이 layer가 inner product layer라고 불리는 이유는 input yl에 대해 output yl+1이 간단한 inner product 들이 모여있는 형태로 표현되기 때문이다. 예를 들어 yl+1,i를 l+1 번째 layer의 i 번째 node라고 한다면, yl+1,i=jwijyl,j으로 표현할 수 있음을 알 수 있다. 그런데 이 값은 사실 vector w와 yl의 inner product로 표현됨을 알 수 있다. 그렇기 때문에 fully connected layer를 inner product라고 부른다. 다시 본론으로 돌아와서 inner product의 output은 input과 weight의 matrix-vector multiplication인 yl+1=Wlyl으로 표현할 수 있다.

따라서 yl+1yl=Wl이고, ylWl=yl이다. 이 값을 통해 실제 return하는 값은 Eyl=Eyl+1Wl와 Ewl=Eyl+1yl이 된다.

두 번째로 많이 사용하는 ReLU non-linearity의 gradient를 계산해보자. 이때 activation function은 마치 하나의 layer가 더 있는 것처럼 생각할 수 있다. 즉 yl+1=max(0,yl)로 표현할 수 있을 것이다. Parameter는 없으니까 생략하면 만약 yl0라면 yl+1yl=1이고, 아니라면 0이 될 것이다. 따라서 yl0라면 Eyl=Eyl+1이 되고, 0보다 작다면 0이 될 것이다.

정리

Deep learning을 다루기 위해서는 가장 먼저 aritifitial neural network의 model에 대한 이해와 gradient descent라는 update rule에 대한 이해가 필수적이다. 이 글에서는 가장 기초적이라고 생각하는 feed-forward network의 model을 먼저 설명하고, paramter를 update하는 gradient descent algorithm의 일종인 backpropagation에 대한 개념적인 설명을 다루었다. 조금 어려울 수 있는 내용이니 다른 글들을 계속 참고하면서 보면 좋을 것 같다.

Reference

출처 : http://sanghyukchun.github.io/


Data-driven marketing, Data-driven decision making, 데이터 중심적 사고 등 데이터를 중심으로 사고하는 것에 대한 중요성이  Business World를 중심으로 급격히 성장하고 있다. 따라서 다양한 분야에서 스스로가 데이터 중심의 사고를 하고 있는지 점검하고, 이를 강화하는 방법이 있는지에 대해 궁금해 하는 사람들이 많을 것이다. 이번 포스팅에서 소개하는 글은 Data-driven의 특성을 정의하고, 사례와 인용을 통해 각각의 특성들이 실제 비즈니스에서 어떻게 응용 되는지 소개하고 있다. (원문)

당신은 데이터 중심적(data driven)인가? 

데이터 중심적 (data driven) 이라는 용어는 오늘날 가장 부각되는 어휘이다. Data Driven은 필자의 최근 저서의 이름이기도 하며, 최근의 학술 연구들은 스스로를 “data driven” 이라고 언급하는 회사들이 그렇지 않은 회사들에 비해 수익성이 좋다는 것을 보여주고 있다. 따라서 데이터 중심적으로 변화하는 것은 노력할 만한 가치가 있는 일이다.

지금까지의 노력에도 불구하고, 필자도 아직 리더가 자신의 조직이 더 잘하기 위해 필요한 것을 발견할 수 있는 벤치마크나 기준을 찾지 못했다.

개인적으로 “data-driven”의 핵심은 조직의 차트를 오르고 내리게 만드는 의사결정을 더욱 잘하게 만드는 데 있다고 본다. 몇 년전, 운 좋게도 많은 의사 결정자 및 의사 결정 그룹과 일할 기회가 있었다. 그들 중 몇몇은 훌륭한반면, 몇몇은 끔찍했지만, 그 일을 하면서, “data-driven의 12가지 특성”을 도출할 수 있었다.


 

데이터 중심적 (Data-Driven) 사고를 하는 사람들의 특성

  • 가능한 가장 낮은 단계에서 의사 결정을 수행
  • 가능한한 여러 상황의 다양한 데이터를 수집
  • 깊이 이해할 수 있도록 데이터를 발전
  • 변화에 대한 인지의 발전
  • 불확실성에 대한 합리적 처리
  • 데이터와 그 영향을 이해하는 능력과 직관의 통합
  • High-quality 데이터와 이를 개선하기 위한 투자의 중요성에 대한 인식
  • 훌륭한 실험자 및 연구자의 자질
  • 의사 결정의 기준이 상황에 따라 변할 수 있다는 점에 대한 인식
  • 의사 결정은 단지 첫 단추에 불과하다는 점의 인식
  • 새로운 스킬, 새로운 데이터, 새로운 기술 (빅데이터, 예측 모델, 메타데이터 관리 등)을 조직 내 주입하기 위한 노력
  • 실수로 부터의 배움

 

이 모든 특성들은 중요하다. 대부분은 그 이유가 자명하지만, 몇몇은 추가적인 설명이 필요하다. 첫번째는 데이터 중심의 회사가 가장 낮은 단계에서 의사 결정을 해야한다는 것이다. 필자와 대화를 나눈 한 임원은 이에 대해 다음과 같이 설명했다. “내 목표는 일년에 6개의 결정만 하는 것 입니다. 이는 내가 가장 중요한 6개를 골라내야 한다는 것이고, 나에게 보고하는 사람은 반드시 데이터와 이에 대한 확신을 갖고 있어야 하며, 그들은 나머지에 대한 의사 결정을 해야 합니다.” 조직 하부에서 의사 결정하는 것을 유도하면 고위직은 가장 중요한 의사 결정을 하는데 필요한 시간을 확보할 수 있다. 또 중요한 것은 하급 직원들에게 의사 결정 권한이 떨어지면, 그들이 이를 위해 더 많은 시간과 관심을 쏟는다는 점이다. 이는 조직적 역량을 올바르게 강화시키고, 업무 환경을 보다 즐겁게 만든다.

두번째, 데이터 중심적 사고를 하는 사람들은 변화에 대한 타고난 감각을 갖고 있다. 가장 단순한 프로세스, 사람들의 응답, 또는 가장 통제 되는 상황조차도 달라진다. 그들은 관리도를 이용하지 않지만, 무슨 일이 일어나고 있는지 이해하려면 변화를 이해해야 한다는 점을 알고 있다. 한 중간급 관리자는 이를 다음과 같이 설명했다. “제가 첫 관리 업무를 맡았을때, 저는 결과물을 놓고 매주 고뇌했습니다. 몇주는 약간 올랐지만, 나머지는 다 하락 했습니다. 저는 상승에 대한 공을 차지하고자 노력했고, 침체가 되면 또 괴로워했습니다. 제 상사는 저에게 이를 그만두라고 했습니다. 확실히 상황은 악화 되었습니다. 모든 것들은 요동친다는 것을 배우는 데는 오랜 시간이 걸렸습니다.”

세번째, 데이터 중심적 사람들은 데이터와 데이터 소스에 대한 높은 수요가 있을 때에 존재한다. 그들은 그들이 내린 결정이 기반이 된 데이터보다 낫지 않다는 것을 안다. 따라서 데이터의 품질과 믿을 만한 데이터 소스를 만드는데 투자한다. (참고) 그 결과, 시간에 민감한 이슈가 발생했을 때를 대비한 준비가 되어 있다. 고품질의 데이터는 다양성을 이해하는 것과 불확실성을 감소시키는 것을 쉽게 해준다. 성공은 실행에 의해 측정 되고 고품질 데이터를 통해 직원들이 의사 결정자의 논리를 이해하고 쉽게 따르게 만든다.

더 나아가, 한 번의 실행은 더 많은 데이터를 얻게 한다. 따라서 데이터 중심적 사람들은 지속적으로 그들의 결정을 재평가하고, 정제한다. 특히 이들은 의사 결정이 틀렸다는 증거가 제시 될 때, 다른 사람들보다 빠르게 대응한다. 이는 데이터 중심적 사람들이 급회전을 한다는 것을 의미하진 않는다. 그들은 결정이 지속 가능하지 않다는 것을 안다.

 

 

이제 거울을 들여다보자. 상단의 리스트를 들여다보고 각각의 특성에 대해 점수를 매겨보자. 정기적으로 잘 따르는 항목은 1점, 전부는 아니지만 대부분 따르는 항목은 0.5점. 한 두번 한 것이라면 점수를 주면 안된다.

만약 당신이 7점 미만이라면, 더 올려야 할 필요가 있다. 각각의 사람이나 조직은 서로 다르기 떄문에 의사 결정을 조직의 하부로 보내는 것부터 시작하기를 추천한다. 이에 따른 이익은 이미 언급하였다. 모든 것을 제어하고 싶어하는 매니저들에겐 아마 매우 힘들고 반직관적일 것이다. 하지만 충분히 시도할 가치가 있다.

둘째로 고품질의 데이터에 투자해라. (참고) 실로 당신의 데이터와 그 출처에 대한 높은 신뢰가 없다면 데이터 중심의 사고를 할 수 없다. 당신의 직관을 반으로 감소시키는 것을 목표로 해라.

마지막 한단계는 스스로를 들여다보는 것이다. 당신의 조직을 위해 똑같은 일을 하는 팀을 참여 시켜라.

<Harvard Business Review>

출처 : http://eunwoopark.com/wp/category/bigdata/ : 사라짐.

'데이터 사이언트' 카테고리의 다른 글

R - 데이터 고급 분석과 통계 프로그래밍  (0) 2016.05.10

(번역) 서버리스 아키텍처

이 글은 마틴 파울러의 웹사이트에 올라온 Serverless Architectures을 번역한 글입니다. 원문이 계속 업데이트 되기 때문에 번역본과 원문을 함께 보시면 더욱 도움이 될 겁니다.


2016년 6월 17일

마이크 로버츠 Mike Roberts
마이크는 뉴욕에 사는 엔지니어링 리더이다. 요즘엔 팀 매니지먼트가 주요 업무이긴 하지만 여전히 클로주어 Clojure 쪽에서 코딩도 하고 소프트웨어 아키텍처 쪽에서도 활발한 의견 개진을 하고 있다. 그는 지금 사람들이 서버리스 아키텍처에 대해 주목하는 현상에 대해 꽤 긍정적이다.
 
아래 태그들을 통해 비슷한 문서들을 찾을 수 있다:
application architecture


서버리스는 요즘 소프트웨어 아키텍처 세상에서는 아주 핫한 토픽입니다. 책들도 나왔고, 오픈소스 프레임워크도 있고, 수많은 벤더들이 프레임워크를 내놨죠. 게다가 아예 서버리스만을 주제로 하는 컨퍼런스까지 생겼습니다. 그런데, 도대체 서버리스가 뭘까요? 그리고 어째서 이 서버리스를 고려해야 (혹은 고려하지 말아야) 할까요? 이 계속 업데이트 되는 문서를 통해 저는 당신이 이러한 질문들에 대한 답을 구할 수 있는 빛을 찾기를 바랍니다.

서버리스란 무엇인가?

소프트웨어 업계에서 늘상 그렇듯이, 서버리스에 대한 명확한 관점은 없습니다. 그리고 아래 두 가지의 다르지만 겹치는 부분이 있는 이러한 견해들 역시도요:

  1. 서버리스는 서버단 로직이나 상태 등을 관리하기 위한 써드파티 애플리케이션 혹은 클라우드 서비스에 현저히 또는 온전히 의존하는 애플리케이션들을 설명하기 위해 쓰였습니다. 주로 리치 클라이언트 애플리케이션(예를 들자면 단일 페이지 웹 애플리케이션이나 모바일 앱 같은 것들)을 가리키는데, 클라우드에서 접근 가능한 ParseFirebase 같은 데이터베이스라든가, Auth0, AWS Cognito 같은 인증 서비스들 같은 거대한 생태계를 사용하는 것들입니다. 예전에는 이러한 서비스들을 (Mobile) Backend as a Service라고 불렀으니, 여기에서는 이들을 그냥 BaaS라고 부르도록 하죠.

  2. 또한 서버리스는 개발자들이 서버단 로직을 개발자들이 짜긴 하지만, 전통적인 아키텍처와는 달리 상태를 저장하지 않는 Stateless 컴퓨팅 컨테이너에 넣고 돌리는 애플리케이션을 의미하기도 합니다. 이러한 애플리케이션은 보통 이벤트 기반으로 작동하고, 한 번 쓰고 버리고, 써드파티에 의해 관리되죠(ThoughWorks는 최근 자사 포스트에서 이렇게 정의했습니다). 이런 방식으로 생각해 볼 수 있는 한가지 방법은 Functions as a Service(또는 FaaS)입니다. AWS 람다는 현재 이 FaaS계의 가장 인기있는 구현체지요. 하지만 다른 것들도 더 있습니다. 여기서는 바로 이 FaaS서버리스의 의미로 사용하도록 하겠습니다.

저는 주로 두 번째 얘기를 할텐데요, 조금 더 새롭기도 하고 우리가 흔히 기술적인 아키텍처에 대해 생각하는 것과 현격한 차이가 있기도 합니다. 게다가 요즘 서버리스라는 것에 대한 수많은 얘기들이 오고가기 때문이기도 하구요.

하지만, 이러한 개념들이 사실은 모두 관련이 있고 하나로 모여들고 있습니다. Auth0가 하나의 좋은 예가 될 수 있겠네요. 처음에 BaaS 형태인 Authentication as a Service로 시작했다가 지금은 Auth0 Webtask를 통해 FaaS 영역으로 들어왔습니다.

게다가 BaaS 형태의 애플리케이션을 개발하는 많은 경우, 특히 모바일 앱과 반대로 리치 웹 앱을 개발하는 경우, 어느 정도 서버단의 커스텀 기능들이 여전히 필요합니다. 특히 당신이 사용하고 있는 BaaS 서비스와 어느 정도 통합을 한다면 FaaS 가 이런 경우 좋은 솔루션이 될 수 있습니다. 이러한 기능들의 좋은 예로는 데이터 유효성 검사(악성 클라이언트로부터 보호하기 위한)라든가 많은 계산 용량을 필요로 하는 작업들(이미지나 비디오 프로세싱 같은 것들)이 있겠지요.

몇 가지 예제

UI 주도 애플리케이션

서버단에 로직이 있는 전통적인 쓰리티어 클라이언트 시스템을 봅시다. 전자상거래 시스템들 같은 것이 좋은 예가 되겠네요. 예를 들자면 온라인 애완동물 용품 사이트 같은 것.

전통적으로 이런 아키텍처는 이런 식으로 생겼습니다. 서버단에 자바로 구현했고, 클라이언트단에는 HTML과 자바스크립트로 구현하죠.

이런 아키텍처에서 클라이언트는 상대적으로 그닥 똑똑하지 않습니다. 대부분의 로직들 – 인증, 페이지 네비게이션, 검색, 트랜잭션 등은 서버단에서 구현을 해놨으니까요.

서버리스 아키텍처에서는 이렇게 보일 겁니다:

엄청나게 간단하게 그린 모델인데요, 그럼에도 불구하고 여전히 수많은 변화들이 일어난 것을 볼 수 있습니다. 여기서 잠깐! 이건 단순히 서버리스 개념을 보여주기 위한 도구로서 만든 그림이지 이게 이런 식으로 아키텍처를 이전해야 한다고 추천하는 건 아니라는 것을 기억해 두세요!

  1. 최초 애플리케이션에서 인증 로직 부분을 빼고 써드파티 BaaS 서비스로 교체했습니다.

  2. 또다른 BaaS의 예로, 상품 리스트 출력을 위해서 클라이언트단이 직접 데이터베이스를 접속하게 했습니다. 이 데이터베이스는 AWS의 Dynamo DB 같이 전적으로 써드파티 데이터베이스가 됩니다. 클라이언트단에서 데이터베이스에 접속할 수 있는 다른 보안 프로파일을 적용하는 방식으로 다른 서버 리소스에서도 데이터베이스에 접근할 수 있게도 할 수 있습니다.

  3. 앞서 언급한 두 가지 포인트는 굉장히 중요한 이 세번째 포인트를 암시합니다 – 쇼핑몰 서버단에 있던 로직들이 이제는 클라이언트단으로 옮겨갔다는 거죠. 예를 들자면 사용자 세션 추적이라든가, 페이지 네비게이션 같은 애플리케이션의 UX 구조를 이해하는 로직이라든가 데이터베이스에서 읽어들인 자료를 사용자 뷰에 맞는 형식으로 변환하는 것들이라든가 하는 것들 말입니다. 이렇게 되면 사실 클라이언트단은 이제 단일 페이지 애플리케이션이 되는 셈입니다.

  4. UX관련 기능들중 어떤 것들은 서버단에 계속 두고 싶을 거예요. 예를 들자면 많은 계산 용량을 필요로 한다든가 대용량 데이터에 접근을 해야 한다든가 하는 것들이죠. 검색 기능을 예로 들 수 있을텐데요, 검색 기능을 위해서 항상 서버를 돌리기 보다는 API 게이트웨이(나중에 다시 설명합니다)를 이용한 FaaS 펑션을 구현해서 HTTP 리퀘스트에 응답하게끔 하면 됩니다. 그렇게 함으로써 우리는 클라이언트단과 서버단에 기능을 두고 상품 데이터가 있는 같은 데이터베이스에서 읽어들이게 할 수 있습니다.

    원래 서버단 기능들을 자바로 구현했고, 이 포스트에서 선택한 FaaS 제공자로서 AWS 람다 서비스를 자바를 지원하기 때문에, 온라인 쇼핑몰의 검색 기능 관련 코드를 서버단에서 람다로 코드를 다시 쓰지 않고도 쉽게 옮길 수 있습니다.

  5. 마지막으로 상품구매 기능을 다른 FaaS 펑션으로 대체할 수 있습니다. 보안상의 이유로 클라이언트단으로 옮기기 보다는 서버단에 이 기능들을 놓는 것이 낫기 때문입니다. 물론 API 게이트웨이를 그 앞에 놓았습니다.

메시지 주도 애플리케이션

다른 예를 하나 더 들자면 백엔드에서 돌아가는 데이터 프로세싱 서비스가 될 겁니다. 지금 당신이 사용자 중심의 애플리케이션을 하나 개발하고 있다고 치죠. 이 애플리케이션은 UI 리퀘스트에 재빨리 반응을 해야 합니다. 하지만 동시에 현재 일어나는 모든 종류의 액티비티들을 로그로 저장하고 싶어합니다. 온라인 광고 시스템을 한 번 생각해 봅시다 – 사용자가 광고를 클릭할 때 사용자를 재빨리 해당 광고의 타겟으로 보내고 싶습니다. 동시에 사용자 클릭이 발생했다는 것을 잡아내서 광고주에게 과금할 수 있어야 합니다.

전통적으로 이런 아키텍처는 보통 광고 서버가 동기적으로 사용자에게 반응(여기서는 그 반응이 어떤 것인지에 대해서는 상관하지 않습니다)하는 동시에 채널을 통해 메시지를 보내서 비동기적으로 클릭 프로세서를 실행시켜 데이터베이스를 업데이트합니다. 데이터베이스 업데이트에는 광고주 예산에서 광고만큼 금액을 집행하는 것들이 있을 수 있겠죠.

그런데, 서버리스 세상에서는 위의 모델이 아래와 같이 바뀝니다.

앞서 예를 든 것과는 차이가 그렇게 많이 나는 것처럼 보이지는 않네요. 우리는 여기서 계속 돌아가는 서버단의 프로세스를 이벤트 주도 형태의 콘텍스트 안에서 돌아가는 FaaS로 바꿨습니다. FaaS 서비스 제공자는 우리에게 서로 연결되어 있는 메시지 브로커(Message Broker)와 FaaS 환경을 제공합니다.

또한 이 FaaS 환경은 동시에 일어나는 클릭들도 펑션 코드를 클릭 이벤트 숫자에 맞게 감지해서 처리합니다. 기존 애플리케이션의 프로세스에 이런 병렬코드 진행 부분이 없었다면 이 새로운 개념을 적용시켜야 할 수도 있습니다.

Function as a Service 뒤집어보기

우리는 이미 FaaS에 대해 여러번 언급을 해 왔습니다. 이제부터는 도대체 그게 뭔지 좀 더 파고 들어갈 때가 됐습니다. 우선 아마존의 람다 서비스에 대한 설명을 좀 보도록 하죠. 번호를 군데군데 매겨놨는데요, 잠시 후에 설명하도록 하겠습니다.

AWS 람다는 서버를 만든다거나 관리할 필요 없이 당신의 코드를 실행시킬 수 있다. (1) … 람다와 함께라면 당신은 어떤 형태의 애플리케이션이나 백엔드 서비스에서도 코드를 돌릴 수 있다. (2) – 관리 비용은 전혀 필요가 없다. 그저 당신의 코드를 업로드하면 람다가 실행에 필요한 모든 것들을 알아서 관리해 주고 (3), 필요하면 스케일링도 해주면서 (4) 계속 높은 가용성을 유지시켜 준다. 당신은 다른 AWS 서비스로부터 자동으로 트리거링을 받게끔 코드를 작성할 수도 있고 (5) 어떤 웹이나 모바일 앱등에서도 이를 직접 호출하여 실행시킬 수 있다. (6)

  1. 기본적으로 FaaS는 당신이 서버 시스템들 없이 또는 서버 애플리케이션 없이 백엔드 코드를 실행시키는 것입니다. 서버 애플리케이션이라는 구절이 바로 핵심적인 차이인데, 이것은 다른 현대적인 아키텍처의 흐름, 컨테이너라든가 PaaS(Platform as a Service) 등을 의미합니다.

    만약 다시 위의 클릭 처리 예제로 돌아간다면, FaaS는 클릭 처리를 담당하는 서버(물리적 머신일 수도 있지만 어쨌거나 실제 그 용도로 쓰이는 애플리케이션)를 서버 프로비저닝이 필요하거나 항상 돌아가야 하는 애플리케이션이 아닌 다른 무언가로 바꾸는 것입니다.

  2. FaaS는 굳이 특정 프레임워크나 라이브러리에 의존해서 코딩하는 것을 필요로 하지 않습니다. FaaS 펑션은 아무 언어 혹은 환경에서도 작동하는 하나의 애플리케이션입니다. 예를 들어, AWS 람다 펑션은 자바스크립트라든가, 파이쎤 혹은 자바, 클로저, 스칼라 등의 아무 JVM 언어들로 구현할 수 있습니다. 또한 당신의 람다 펑션은 설치 아티팩트와 함께 묶여있기만 한다면 다른 프로세스를 통해서 아무 언어나 실행시킬 수 있습니다. 유닉스 혹은 리눅스 환경에서 컴파일이 가능하다면 말이죠(나중에 Apex를 다뤄보겠습니다). FaaS 펑션은 상태라든가 굉장히 제한적인 아키텍처를 갖고 있습니다. 상태라든가 실행 시간 같은 것들을 고려해야 한다면 말이지요. 이건 잠시 후에 다시 설명하기로 하죠.

    다시 앞서의 클릭 프로세스로 돌아가 봅시다. FaaS로 옮겨갈 때 코드를 변경해야 하는 유일한 부분은 바로 main 메소드 혹은 startup 코드 부분입니다. 이 부분은 필요가 없고, 대신 최상위 계층의 (메시지 리스너 인터페이스를 구현한) 메시지 핸들러로 변경하면 됩니다. 이 부분이 유일한 코드 변경점이죠. 코드의 나머지 부분은 (예를 들어 데이터베이스에 접근한다든가 하는 부분) FaaS 세상에서도 변함없이 똑같습니다.

  3. 이제 우리는 실행시킬 서버 애플리케이션이 없습니다. 그래서 설치 과정 역시도 전통적인 애플리케이션과 굉장히 달라지게 됩니다. 그저 코드를 FaaS 제공자로 업로드하면 나머지는 그쪽에서 다 알아서 하게 되죠. 지금 현재로서는 이것은 새로 수정한 코드 혹은 새로 만든 코드를 .zip 파일 혹은 JAR 파일로 묶어서 올리는 것을 의미하구요, 개별 FaaS 서비스 제공자의 내부 API를 통해 이 수정 사항을 실행시키게끔 호출하는 것으로 보면 됩니다.

  4. 수평적 스케일링은 이제 완전히 자동화가 됐구요, 서비스 제공자가 다 알아서 합니다. 만약에 당신의 시스템이 100개의 리퀘스트를 동시에 처리해야 한다면, 내 쪽에서 별도의 설정 같은 것을 하지 않아도 서비스 제공자가 알아서 다 합니다. 이렇게 당신의 펑션을 실행하는 컴퓨팅 컨테이너는 일시적으로 FaaS 제공자가 관리하고 파기하는 형태여서 순전히 런타임에서만 잠깐 필요한 정도가 됩니다.

    다시 우리의 클릭 프로세서 예제로 돌아가보죠. 사용자가 평소보다 한 열 배 정도는 광고 클릭을 더 많이 한다고 가정해 봅시다. 기존의 클릭 프로세싱 애플리케이션은 이걸 처리할 수 있을까요? 다시 말해서 당신의 코드는 한 번에 여러 개의 메시지를 처리할 수 있을까요? 심지어 우리가 할 수 있다고 해도 애플리케이션의 실행 인스턴스가 하나만 있다면 이걸 감당할 만큼 충분할까요? 만약 다중 프로세스를 돌릴 수 있다면 우리는 이걸 자동으로 오토 스케일링 설정을 해 놓아야 할까요 아니면 수동으로 그때그때 설정해야 할까요? FaaS라면 당신은 그저 펑션을 작성할 때 병렬 프로그래밍을 가정하고 작성하면 됩니다. 그러면 FaaS 제공자는 스케일링이 필요할 경우 알아서 다 해주죠.

  5. FaaS에 있는 펑션들은 서비스 제공자가 정의한 이벤트 타입에 의해 실행될 수 있습니다. 아마존 AWS의 경우에는 S3 파일 업로드 이번트, 스케줄링 작업에 따른 시간, Kinesis와 같은 메시지 버스에 메시지가 추가되는 이벤트 같은 것들이 있습니다. 이럴 경우 당신의 펑션은 보통 연결되어 있는 특정 이벤트에 대응하는 파라미터 값들을 제공해야 합니다. 예시로 든 클릭 프로세서의 경우에는 이미 FaaS에 대응하는 메시지 브로커를 사용하고 있다고 가정합니다. 만약 그렇지 않다면 바꿔야 하구요, 이 경우에는 메시지 생성하는 로직을 수정할 필요가 있습니다.

  6. 대부분의 서비스 제공자들은 펑션들이 HTTP 리퀘스트에 응답을 보내게끔 구현되어 있습니다. 예를 들자면 API 게이트웨이 같은 형식으로 말이지요. 이런 것들에는 AWS API 게이트웨이, Webtask 등이 있습니다. 우리는 앞서 예시로 든 애완동물 온라인 쇼핑몰에서 검색 기능과 구매 기능에 이용하고 있죠.

상태

FaaS 펑션을 내 로컬 머신 혹은 로컬 인스턴스에서 돌릴 때는 굉장히 제한적입니다. 즉, 어떤 펑션을 실행시킬 때 당신이 생성한 어떤 프로세스 혹은 호스트 상태가 다음에 이어지는 펑션으로 어떤 식으로든* 전달되지 않는다고 가정해야 합니다. 이것은 RAM 안에 저장된 상태도 포함하구요, 로컬 디스크에 뭔가를 저장하는 어떤 형태의 상태 역시도 포함합니다. 다시 말해서 설치 유닛 관점에서 *FaaS 펑션은 상태를 저장하지 않습니다(Stateless).

이것은 애플리케이션 아키텍처에 지대한 영향을 줍니다. FaaS가 유일한 건 아니지만요 – 12요소 앱 개념은 정확하게 똑같은 제한점을 갖고 있습니다.

그렇다면 이러한 제한요소를 인정한다고 할 때, 어떤 대안이 있을까요? 보통 FaaS 펑션은 원래 상태 저장기능이 없어서(Stateless) 단순히 입력값을 다른 출력값으로 변경시킨다거나 또는 데이터베이스, Redis 같은 크로스플랫폼 캐시, 혹은 S3 같은 네트워크 파일 스토리지 같은 것들을 통해 리퀘스트 전반에 걸쳐 상태를 저장시키고 그걸 이용해서 좀 더 사용자 요청을 처리합니다.

실행 기간

FaaS 펑션은 개별 실행에 있어 보통 제한시간이 있습니다. 현재 AWS 람다의 경우에는 5분 이상 걸리는 펑션은 실행에 실패하게끔 되어 있구요, 만약 5분 이상 걸릴 경우 자동으로 폐기됩니다.

이것은 오랜 시간을 필요로 하는 작업이라면 새롭게 아키텍처를 변경하지 않는 이상 FaaS 펑션에는 적합하지 않다는 것을 의미합니다. 다시 말해서 전통적으로는 하나의 큰 펑션으로 만들어서 그 안에서 모든 것을 다 처리하는 펑션으로 만들었다면 이제 FaaS에서는 이것을 잘게 쪼개서 각각 별도로 처리하는 형태로 구조를 변경해야 한다는 것이죠.

초기 실행 지연

현재 FaaS 펑션이 리퀘스트에 응답하는데 걸리는 시간은 여러 가지 요소들에 의해 결정되긴 하지만 대략 10ms 에서 2분 정도 사이가 될 겁니다. 딱히 좋은 얘기는 아닌 것 같기는 한데, 조금 더 구체적으로 들어가 보도로 하죠. AWS 람다의 예를 들어 봅시다.

만약에 자바스크립트나 파이썬으로 펑션을 구현했고 그 펑션의 크기가 대략 1천 줄 미만의 코드량으로 그다지 크지 않다면, 실행에 필요한 시간은 아무리 많아야 10ms 에서 100ms를 넘지 않을 겁니다. 펑션의 크기가 커진다면 아무래도 종종 시간이 오래 걸리겠죠.

만약 람다 펑션을 JVM 위에서 구현했다면 종종 10초 이상 걸리는 응답시간을 보일 겁니다. 아무래도 JVM이 구동되기 위해 필요한 시간이겠죠. 하지만, 이것은 아래와 같은 상황에서만 일어나는 상황입니다.

  • 펑션을 자주 실행시키지 않는 경우 – 각 실행 주기가 10분을 넘는 경우
  • 갑자기 트래픽이 늘어나는 경우 – 초당 10개의 리퀘스트를 처리하다가 갑자기 초당 100개의 리퀘스트를 처리하는 식으로 짧은 시간 안에 급격하게 트래픽이 증가하는 경우

전자 같은 경우에는 매 5분 정도마다 핑 리퀘스트를 날려서 계속 서버가 살아있게 하는 식의 핵으로 해결할 수 있습니다.

그렇다면 후자의 경우에 이런 것들이 문제가 될 수 있을까요? 애플리케이션이 트래픽을 처리하는 스타일에 따라 달라질 겁니다. 예전 팀에서는 자바로 비동기 방식의 메시지 처리 람다 애플리케이션을 만들어서 하루에도 수백만개의 메시지를 처리했습니다. 초기 실행 지연 같은 것에는 아무런 걱정이 없었지요. 그건 지연시간이 낮은 트레이딩 애플리케이션을 개발한다면 딱히 이 상황에서는 FaaS를 고려할 이유가 없습니다. 무슨 언어로 개발하든지간에요.

이런 문제가 당신이 개발한 애플리케이션에서 생길지 아닐지는 모르겠지만, 실제 운영 환경에서와 같은 트래픽으로 테스트를 해 볼 필요는 있어요. 그래야 실제 퍼포먼스를 측정할 수 있죠. 만약 당신의 유즈 케이스가 지금 잘 동작하지 않는다면 한 두어달 쯤 후에 다시 시도해 볼 수 있습니다. FaaS 서비스 공급자가 개발해야 할 영역이거든요.

API 게이트웨이

FaaS가 갖는 특징들 중 하나는 앞서 살짝 언급한 API 게이트웨이입니다. API 게이트웨이는 HTTP 서버로서 설정을 통해 라우팅 정보와 엔드포인트를 정의하고 각각의 라우트는 FaaS 펑션에 연결 시킵니다. API 게이트웨이가 리퀘스트를 받았을 때, 리퀘스트와 일치하는 라우팅 정보를 찾아서 그에 맞는 FaaS 펑션을 실행시킵니다. 보통 API 게이트웨이는 HTTP 리퀘스트 파라미터로부터 FaaS 펑션에 필요한 입력 인자를 매핑합니다. 그렇게 함으로써 API 게이트웨이는 FaaS 펑션의 결과값을 HTTP 응답객체에 실어서 최초 요청자에게 반환합니다.

AWS는 API 게이트웨이 서비스를 갖고 있구요, 다른 제공자 역시도 비슷한 기능을 보유하고 있습니다.

API 게이트웨이는 단순히 리퀘스트를 라우팅하는 기능 이외에도 인증 절차를 수행하고, 입력값에 대한 유효성 검사를 수행하며 응답 객체와 매핑을 시키는 등의 역할을 하기도 합니다. 당신의 거미줄 같은 감각은 어쩌면 이게 실제로 좋은 생각인지 아닌지 궁금해 할 수도 있습니다. 잠시 후에 다시 얘기해 보도록 하죠.

API 게이트웨이와 FaaS 조합의 한가지 유즈 케이스는 HTTP를 앞세운 마이크로서비스 형태가 될 겁니다. 서버리스는 여기서 스케일링과 관리 그리고 FaaS 펑션이 가져다 주는 여러가지 잇점을 담당하죠.

현 시점에서 API 게이트웨이 도구는 아직 처절할 정도로 성숙하지 않았습니다. 그렇긴 해도 API 게이트웨이와 함께 애플리케이션을 개발하는 것이 그다지 어렵거나 한 것은 아닙니다.

도구들

API 게이트웨이 도구들이 아직 성숙하지 않았다는 것은 이미 언급했구요, 이것은 전반적으로 서버리스 FaaS 시장에 있어서 공통적인 현상입니다. 하지만 예외는 있죠. 그 예가 바로 Auth0의 Webtask인데요 개발자 UX에서 엄청난 강점을 갖고 있습니다. Tomasz Janczuk은 최근에 있었던 서버리스 컨퍼런스에서 굉장히 좋은 데모를 보여준 적이 있습니다.

디버깅과 모니터링 역시 이 서버리스 애플리케이션에서는 해결해야 할 숙제들입니다. 이 포스트의 뒷부분에서 다뤄보도록 하죠.

오픈 소스

서버리스 FaaS 애플리케이션의 주요 잇점들 중 하나는 바로 투명한 실행 환경 공급에 있습니다. 아직 오픈 소스들은 현재 여기에 그다지 관련이 있지는 않습니다. 도커와 같은 컨테이너들 말이죠. 조만간 우리는 유명한 FaaS / API 게이트웨이 플랫폼이 회사내 on-premise에서 돌아간다거나 개발자의 컴퓨터에서 돌아간다거나 하는 것들을 볼 수 있을 겁니다. IBM의 OpenWhisk는 좋은 예가 될 수 있는데요, 이것이 어떤 대안이 될지 아닐지 지켜보는 것도 꽤 흥미로울 겁니다.

실행 환경 구성과는 별개로 FaaS 펑션을 정의하고, 설치하고 실행시키는데 도와주는 도구들과 프레임워크들은 이미 오픈 소스로 많이 나와 있습니다. 예를 들어 서버리스 프레임워크는 실제로 동작하는 API 게이트웨이와 람다를 AWS에서 제공하는 형태보다 훨씬 더 쉽게 사용할 수 있게 해줍니다. 자바스크립트를 좀 지나치게 쓰긴 했는데, 만약 자바스크립트와 API 게이트웨이 조합으로 애플리케이션을 개발한다면 꼭 한 번 봐 둘만 합니다.

또다른 예로는 Apex가 있습니다. 이 프로젝트는 AWS 람다 펑션들을 손쉽게 만들고, 설치하고, 관리하자라는 슬로건을 갖고 있습니다. Apex가 갖는 재미있는 요소들 중 하나는 아마존에서 직접 지원하지 않는 언어들을 람다 펑션 차원에서 지원하게끔 해준다는 겁니다. 예를 들자면 Go 언어 같은 것들이죠.

서버리스가 아닌 것은?

직금까지 이 글에서 저는 서버리스Backend as a Service (BaaS)Functions as a Service (FaaS)의 합집합이라고 정의했습니다. 또한 주로 FaaS 쪽을 중점으로 해서 이야기를 풀어나갔지요.

이제 가장 중요한, 무엇이 이득이고 무엇이 손해인지에 대해 얘기하기 전에 이 서버리스의 정의에 대해 조금만 더 살펴보고자 합니다. 적어도 무엇이 서버리스아닌지에 대해 얘기해 보죠. (최근의 저를 포함해서) 몇몇 사람들이 이러한 것들에 대해 혼동했던 것을 봐 왔고, 좀 더 명확하게 하는 것도 좋은 생각 같습니다.

PaaS와 비교

앞서 잠깐 서버리스 FaaS 펑션은 12요소 애플리케이션과 비슷하다고 했는데요, 그렇다면 Heroku와 같은 또다른 PaaS라고 할 수도 있을까요? 간단하게 대답하기 위해 Adrian Cockcroft의 트윗을 인용하겠습니다.

만약 당신의 PaaS가 20ms 이내에 인스턴스를 실행시켜서 0.5초 동안 원하는 기능을 실행시킬 수 있다면 그땐 그걸 서버리스라고 부르세요.

다른 말로, 대부분의 PaaS 애플리케이션들은 매 리퀘스트마다 애플리케이션 전체를 올렸다 내렸다할 수 있게끔 설계되지 않았습니다. 반면에 FaaS 플랫폼은 정확하게 그렇게 하죠.

좋습니다. 만약 제가 훌륭한 12요소 애플리케이션의 개발자라면 딱히 코딩을 하는데 있어서 별 차이점은 없을 거예요. 사실입니다. 하지만 가장 큰 차이는 어떻게 당신의 애플리케이션을 운영하는가에 있습니다. 우린 모두 데브옵스 관점에 충실한 엔지니어들이고 개발에 대해 생각하는 것 만큼 운영에 대해서도 생각하고 있습니다, 그렇죠?

운영 측면에서 FaaS와 PaaS의 핵심적인 차이는 바로 스케일링입니다. 대부분의 PaaS에서 당신은 여전히 스케일링을 고민해야 하죠. 헤로쿠의 예를 들자면 다이노스 Dynos 몇개를 돌리고 싶은가를 고민해봐야 합니다. FaaS 애플리케이션에서 이부분은 완전히 투명합니다. 심지어 당신이 PaaS 애플리케이션을 스케일링 완전 자동화로 설정한다 하더라도 개별 리퀘스트 수준에서 이런 스케일링을 하진 앟아요(물론 당신이 굉장히 특별하게 트래픽 프로필을 설정해 놓았다면 얘긴 달라집니다). 따라서 FaaS 애플리케이션은 이렇게 비용이 연계가 될 때 굉장히 효율적입니다.

이런 잇점들이 있다면 왜 계속 PaaS를 쓰려고 하죠? PaaS를 쓸 이유들이 여러가지 있겠지만 아마도 도구들 그리고 API 게이트웨이의 성숙도가 가장 큰 이유들이 될 겁니다. 더군다나 PaaS에 구현한 12요소 애플리케이션들은 최적화를 위해 앱 내 읽기전용 캐시를 사용하겠죠. 이것은 FaaS 펑션에서는 사용할 수 없는 기능입니다.

#NoOps

서버리스NoOps를 의미하는 것은 아닙니다. 서버리스 토끼구멍을 얼마나 깊이 파고 들어가는가에 따라 아마도 내부 시스템 관리자가 없다는 것을 의미할 거예요. 여기서 우리는 두가지 중요한 것을 고려해야 합니다.

먼저 Ops는 서버 관리 이상의 그 무언가를 의미합니다. 적어도 모니터링, 설치, 보안, 네트워킹 등을 의미하기도 하죠. 그리고 종종 시스템 스케일링과 어느 정도의 운영 시스템 디버깅까지를 포함하기도 합니다. 이런 문제들은 서버리스 애플리케이션으로 간다고 해도 여전히 존재하고 이를 해결할 전략이 필요하죠. 어떤 면에서는 Ops서버리스 환경에서 좀 더 어려운 일이 될 수도 있습니다. 왜냐하면 모든 것들이 전부 새롭기 때문이죠.

다음으로 시스템 관리자가 여전히 필요하다면 서버리스를 위해서는 아웃소싱을 하면 그만입니다. 딱히 나쁘진 않아요 실제로 우린 여러번 아웃소싱을 해 왔으니까요. 하지만 구체적으로 당신이 무엇을 하려고 하는가에 따라 이건 좋을 수도 있고 나쁠 수도 있습니다. 어느 시점에서 당신은 시스템 관리자가 당신의 애플리케이션을 지원할 필요가 있다는 것을 알아야 할 지 모릅니다.

Charity Majors는 이와 관련해서 최근 있었던 서버리스 컨퍼런스에서 좋은 발표를 해 줬습니다. 저는 온라인에 이 발표가 올라오면 꼭 확인해 보기를 권장합니다. 그 전까지는 이 글이 글을 읽어보면 좋겠네요.

Stored Procedures as a Service

또다른 흥미로운 주제는 서버리스 FaaS가 Stored Procedures as a Service라는 겁니다. (이 글에서 사용한 것들을 포함해서) FaaS 펑션의 많은 예제들이 주로 데이터베이스에 접근하기 위한 코드들이기 때문이 아닐까 생각합니다. 만약에 겨우 이정도가 우리가 FaaS를 사용하는 이유라고 한다면 이 네이밍은 적당할 지도 모르겠군요. 하지만 이건 FaaS의 서브셋에 불과할 뿐더러 만약 이런 용도로만 사용한다면 뭐랄까 조금은 맞지 않습니다.

이것은 어찌 보면 Stored Procedure가 갖는 동일한 문제를 FaaS 역시도 가질 수 있다는 것을 고려해 볼 필요가 있습니다. Camille이 트윗에서 언급한 것과 같은 기술적 부채들도 포함해서 말이지요.

만약에 서버리스 서비스가 마치 Stored Procedure 처럼 변한다면 이건 곧바로 엄청난 기술적 부채가 될 거라는 걸 생각해 보자구.

Stored Procedure를 사용하는 것에서 오는 수많은 교훈들이 있습니다. 그것들은 FaaS에서 반드시 되돌아보고 적용이 가능할지 아닐지를 결정해야 할 것들이지요. Stored Procedure들은:

  1. 종종 벤더 종속적인 언어를 요구하거나, 적어도 벤더 종속적인 프레임워크 혹은 언어로 확장할 필요가 있습니다.
  2. 데이터베이스 콘텍스트 안에서 실행시켜야 하기 때문에 테스트가 어렵습니다.
  3. 일급 애플리케이션으로서 다루기 까다롭고 버전 관리도 힘듭니다.

이러한 제약사항들이 모두 Stored Procedure를 구현하는데 있어서 적용되는 것은 아닐 겁니다. 하지만 지금까지 제 경험상 수많은 문제들을 읽으켰던 것은 사실이예요. 그렇다면 이것을 FaaS에 어떻게 적용을 시킬 수 있는지 살펴봅시다.

1번 항목은 FaaS 구현에 있어서 큰 걸림돌은 아닙니다. 그냥 그런 부분들을 걷어내면 그만이죠.

2번 항목에서 우리는 코드만 쓰기 때문에, 단위 테스트는 다른 코드들과 마찬가지로 쉽습니다. 통합 테스트는 다른 (그리고 정당한) 문제예요. 이건 나중에 얘기해 봅시다.

3번 항목에서 다시금 FaaS 펑션은 코드일 뿐이기 때문에 버전 관리도 괜찮습니다. 하지만 애플리케이션 패키징 측면에서 봤을 때 아직 어떤 성숙한 패턴이 나오지는 않았어요. 앞서 언급했던 서버리스 프레임워크는 자체적으로 이런 패키징 형태를 제공합니다. AWS는 2016년 5월에 열렸던 서버리스 컨퍼런스에서 패키징 관련해서 Flourish라는 이름으로 작업중이라고 발표했습니다. 하지만 이건 뭐 나와 봐야 아는 거겠죠.

이 문서는 지속적으로 진화합니다. 저는 수시로 이 문서를 업데이트할 예정입니다. 그렇게 해서 좀 더 많은 서버리스 아키텍처와 관련한 장단점들을 포함한 주제들을 이 문서에 담길 희망합니다. 아마도 향후 일이년 이내에 좀 더 서버리스 관련 주제들이 발전하지 않을까 싶네요.

이 주제와 관련해서 우리가 어떻게 업데이트 하는지를 알고 싶다면 우리 사이트의 RSS 피드, 제 트위터 피드 또는 마틴 파울러의 트위터 피드를 주목해 주세요.


알림

이 글을 쓰는데 도움을 주신 분들께 감사 드립니다: Obie Fernandez, Martin Fowler, Paul Hammant, Badri Janakiraman, Kief Morris, Nat Pryce, Ben Rady.

이 새 기술에 적당히 반론도 해 주시고 격려도 해주신 Internet Media의 제 전 팀원들께 감사 드립니다: John Chapin, Pete Gieser, Sebastián Rojas and Philippe René.

마지막으로 이 주제와 관련해 여러 생각들을 피력해 주신 모든 분들, 특히 제가 언급한 분들께 감사 드립니다.

리비전

2016년 6월 17일: 서버리스가 아닌 것은? 섹션 추가
2016년 6월 16일: Functions as a Service 뒤집어 보기 섹션 추가
2016년 6월 15일: 첫번째 버전 발행 – 몇가지 예제들


Lean 개발방법론 ( Agile의 하나 )

구체적인 개발 프로세스를 정의하지 않고 철학적인 접근 방식을 정의하고 있다. 즉, 린 개발 방식은 개 발 방법론이라기 보다는 사고방식이란 용어가 더 적합하다 하겠다. – 린 사고 방식(Lean Thinking)

1. 특징
린은 낭비를 발견하고 제거함으로써 어떻게 고객에게 가치를 빠르게 제공할 수 있을 것인가에 대한 생각이자 사고방식이다. 제조분야에서 생산성향상을 위해 사용하는 린워칙을 S/W 개발에 적용하여 낭비요소를 제거하자는 내용. 결론으로 S/W개발의 가장 큰 낭비는 결함이고 결함을 줄이는 좋은 방법은 애자일 방법론 이라는 것이다.

2. Lean에서 대표적 낭비요소
– Transportation
– Inventory
– Motion
– Waiting
– Over Production
– Over Processing
– Defects / Rework

3. Lean S/W의 7가지 개발원칙
– 낭비를 제거하라: 파레토법칙에 의거하여 개발에 정말 중요한 20%에 집중하고 낭비되는 요소 제거
– 품질을 내재화하라: TDD를 통해 코드의 실수를 방지, 빅뱅통합을 버리고 지속적인 통합과 중첩된 동기화 기법사용
– 지식을 창출하라: 과학적 방법 사용, 모든 사람들이 따라하고 잘 알려진 실천법을 표준에 포함하되, 누구든지 표준에 도전하고 변경하도록 장려.
– 확정을 늦춰라: 마지막까지 변화를 수용할 수 있도록 코드 작성, 의존성을 깨뜨리고 옵션을 유지하라.
– 전체를 최적화하라: 고객요구에서 S/W 배포까지 전체 가치흐름에 초점을 맞춰라.
– 사람을 존중하라: 효과적인 리더십 제공하고 팀은 자부심, 책임감, 신뢰, 칭찬을 통해 번성한다.
– 빨리 인도하라: 신속한 인도, 고품질, 저비용은 공존할 수 있다. 일의 양을 할 수 있는 만큼으로 제한하라.

4. Lean S/W 개발과 칸반
– 칸반은 생산시스템에서 일하는 작업자들이 어떤 작업을 해야 하는지 알려주는 작업지시서에 해당
– Lean S/W 개발에서 칸반을 활용하게 되면 다음과 같은 장점을 얻을 수 있다.
– 워크플로우를 가시화한다: 일을 작게 나누고 보일 수 있게 나열한다.
– 작업중인 것을 제한한다: 워크플로우상에 얼마나 많은 항목이 진행되고 있는지 제한을 둔다.
– 작업에 소요되는 시간을 측정한다: 한 항목을 완료하는데 걸리는 평균시간, 예측가능하고 소요시간을 최소화하기 위해 프로세스를 최적화 한다.

5. 애자일 방법론과 린 개발방법의 공통점
– 요구사항의 변화를 적극적으로 수용
– 제품을 고객에게 빠르게 전달하여 고객 가치와 만족도를 높이는데 목적

6. 애자일 방법론과 린 개발방법론의 차이점
– 애자일 촛점: 개인별 또는 팀간에 고객과 협업하고 빠른 개발을 수행하는 것에 초점(고객과 협업)
– 린의 촛점: 고객의 관점에서 전체 프로세스 상에서 낭비를 제거하여 고객 가치를 높이는 것에 우선 순위(전적으로 고객 관점)


'생산성' 카테고리의 다른 글

3M의 혁신적 문화  (0) 2016.05.16

출처 : http://www.zdnet.co.kr/column/column_view.asp?artice_id=20160614172904


칼럼

서버리스(Serverless)가 온다!

  • 윤석찬 AWS 테크 에반젤리스트
  • 입력 : 2016.06.14.17:40
  • 수정 : 2016.06.14.17:40
  • 228

지난 칼럼 '클라우드 기술에 대한 세가지 패러다임 변화'에서 ‘서버 없는 클라우드 함수의 등장’이라는 변화를 소개했다. 이러한 새로운 패러다임은 개발자들에게 큰 수고와 비용 없이도 좀 더 빠르고 민첩하게 다양한 애플리케이션을 만들고, 서비스 운용을 위한 확장성 및 가용성에 대한 수고와 비용을 없애는 방향으로 바뀌고 있다.

이러한 변화를 가장 극적으로 보여준 것이 바로 지난 5월말 뉴욕에서 있었던 서버리스컨퍼런스(Serverless Conference)다. 일반적으로, 회자되는 기술의 유행 방식은 선두 주자가 혁신적인 서비스를 내면, 경쟁적으로 유사한 서비스가 만들어지고, 오픈 소스로 된 관련 도구가 증가하면서 개발자들이 여기에 동조하고, 콘퍼런스에서 다 같이 만나는 패턴인데,이는과거에도 종종 있었다.

2014년 AWS람다(Lambda)가 이러한 개념을 처음 선 보인 이후로, 많은 클라우드 업체들이 이를 벤치마킹한 서비스를 줄줄이 내놓고 있다. 많은 개발자들은 관련된 코드 예제들을 오픈 소스로 공개하고, 급기야는 Serverless FRAMEwork, CloudiaJS 같은 서버리스 오픈 소스 개발 프레임워크가 계속 나오고 있다. AWS에서 Lambda와 API Gateway 서비스 개발을 총괄하고 있는 팀 와그너(Tim Wagner)는 서버리스 콘퍼런스키노트 발표에 앞서 물리 서버를 부숴버리는 상징적인 퍼포먼스를 보여 주기도 했다.

물리적 서버를 부수는 퍼포먼스를 하고 있는 팀 와그너? 출처: @samkroon

물리적 서버를 부수는 퍼포먼스를 하고 있는 팀 와그너? 출처: @samkroon

■ Serverless != No Server

물론 서버리스(Serverless)라는 말 자체가 서버가 필요 없다는 뜻은 아니다. 클라우드에서도 서버는 존재하고 있고, 다만 고객이 스스로 관리해야 하는 서버 혹은 콘테이너가제로(0)에 수렴한다는 의미다. 따라서, 서버리스란 오로지 이벤트에 따라 동작하는 클라우드 기반의 나노 수준 (최근 회자되는 마이크로서비스가 가진 크기를 생각해서) 서비스 단위의프로그램 코드만을 개발하고 배포에 집중한다는 의미이다. 기존의 PaaS(Platform as a Service)는 복잡한 모놀리식(Monolithic) 애플리케이션을 지원했다는 점에서, 무상태(Stateless)는 서버리스의특징과 대비된다.

이유는 간단하다. 더 빠르게 움직이기 위해서다. 이러한 특징은 인프라 설치, 운용, 확장성 고려, 복잡한 배포 및 모니터링 등 많은 관리 업무를 줄이고, 민첩하게 만들고 배포하려는 회사 혹은 팀에게 적합하다.

예를 들어, AWS Lambda는 가장 선두에 있는 서비스로서 Node.js, Java, Python 코드를 올리기만 하면, 코드가 실행될 때 마다 5분 안에 실행하면서 100ms 단위로 과금한다. 다른 AWS 서비스의 이벤트를 처리(예를 들면, Amazon S3에 이미지가 올라오면 썸네일을 만드는 기능을 동작)하거나, Amazon API Gateway로 들어오는 HTTP 요청에 대해서도 실행할 수 있다. 올려진 코드에 대한 버전 기능, 배치 작업을 위한 Cron 기능등을 제공하고, 매월 100만 밀리세컨드에 대해 무료로 제공하기에 테스트 개발에도 적합하다.

모바일 앱을 위한 서버없는백엔드 아키텍처 사례(출처: AWS 한국 공식 블로그)

모바일 앱을 위한 서버없는백엔드 아키텍처 사례(출처: AWS 한국 공식 블로그)

따라서, Amazon API Gateway와 AWS Lambda를 조합하고, 여기에 Amazon 기존 서비스를 연계해서 새로운 아키텍처를 구성할 수 있는데, 이것을 소위 ‘서버리스 아키텍처’라고 부르고 있다. (마치 다양한 요리를 할 때 필요한 재료가 필요한 것처럼, AWS는 최소 단위(primitives)라고 부르는 다양한 서비스로 만들고, 개발자들이 이를 자유롭게 조합하여, 새로운 아키텍처를 설계 구성하도록 하는 서비스 철학을 가지고 있다)

■ 진화하는 서버리스 개발 생태계

서버리스 아키텍처나 프레임워크는아직 초기 단계다. 해결해야 할 사항도 적지 않다. 예를 들어, 기존 서버 기반 SW 플랫폼 개발 프레임워크만큼, 통합 개발 환경(IDE)나 테스팅, 디버깅이 편리하지 않다. 개별 클라우드 함수의 크기나 성능에 따른 메모리 사이징(그에 따른 CPU 및 네트워크 사용량) 및 함수 기능을 어디까지 세분화 할 것인가에 대한 기준도 명확하지 않다.

이런 부분은 서버리스 아키텍처에 대한 다양한 논의가 진행되고, 개발자 생태계가 커지면서 각종 지원 개발 도구가 나온다면 자연스럽게 해결될문제라고 생각한다.

하지만, 가장우선적으로서버리스에 대한 개념과 목적을 명확하게 하는 것이 중요하다. 못을 박기 위한 도구인 망치를 가지고, 음식을 만들려는 우를 범하지 않기 위해서다. 팀 와그너는서버리스 콘퍼런스키노트 중 아래와 같이서버리스선언문(Serverless Manifesto)을 소개하였다.

  • 함수(Function)가 서비스의 기본 배포 및 확장 단위이다.
  • 프로그래밍 모델에서 물리 서버, 가상 서버 및 콘테이너에 대한 의존성을 제거하라
  • 데이터 스토리지는 어딘가 무제한으로 있다고(사용한다고) 가정하라
  • 사용자가 아닌 오로지 요청(Request)에 대해서만 확장하라
  • 요청이 없는데 돈을 낼 필요가 없다(가상 서버나 콘테이너도 여전히 비효율적이다).
  • 함수의 실행은 어디서나 가능하므로, 장애 복원력을 가지도록 만들어라
  • BYOC(Bring your own code) ?나만의 서비스를 책임지고 만들 수 있다!
  • 통계 수집 및 로그 취득은 보편적인 필수 사항이다.

이와 함께 Flourish라는 오프 소스 서버리스 프레임워크를 곧 공개할 것이라고 밝혔다. 이 프레임워크는 마이크로 서비스의 형식을 정의하고, 기존 IDE와 통합하여 빌드 및 ZIP 파일 기반 배포를 할 뿐만 아니라 하나의 대시보드에서 모니터링 및 요금 집계가 가능한 현실적인 서비스 기능을 통합 할 예정이다. 또한 프로그램 코드와 버전 설정을 조합에 의한 일관된 롤백 기능도 제공한다. 벤더 중립적인 API 서비스 참조 역할도 하면서, 코드 작성 및 배포에만 집중되어 있는 기존 프레임워크의 대안이 될 수 있을 것이다.

Flourish가 중립적인 프레임워크로 자리잡더라도 다른 클라우드 업체들도 비슷한 수준의 서버리스 프레임워크를 내놓을 가능성이 높다. 기존의 개발자 커뮤니티에서 만들어지는 프레임워크 역시 생태계 확대에 이바지할 것으로 예상된다.

■ 서버리스의 대중화의 필수 조건은?

서버리스 개발 생태계 확대를 위해서는 기존 벤더 기반 서버리스 컴퓨팅 환경과 스토리지 서비스에서 개발자 생태계 기반 프레임워크와 개발 도구의 제공이 확대되는 단계도 중요하다.하지만 궁극적으로 서버리스 킬러 응용 프로그램(Killer Application)이 나와야 한다.

최근에 Slack을 기반으로 하는 채팅봇애플리케이션이나 Amazon Echo와 Alexa 그리고 AWS Lambda를이용한음성인식서버리스 애플리케이션이 늘어나는 것은 고무적인 현상이다. 테크크런치기사에서 언급한, Amazon Echo의 음성 인식 API인 Alexa Skills과 AWS Lambda를 이용한 앱(Skills)이 연초 135여개에서 1,000여개로 늘어났다는 것이 바로 그러한 예이다.

AWS Lambda의 이용 사례도 극적으로 늘고 있다. 여성 패션 사이트인 Bustie는 수백만의 사용자가 방문하는 웹 사이트를 Amazon S3 기반으로 만들고 필요시 동적 데이터를 Lambda로 처리한다. 광고 리타게팅 플랫폼인 AdRoll 역시 매달 300TB의 압축 데이터를 S3에 저장하는데, 호출 데이터 저장 시 Lambda를 사용한다. 실시간 동영상 인코딩 업체로 유명해진 스타트업인 Periscope는 포르노 같은 유해 영상인지 여부를 3초 단위로 파악해서 차단하는 기능에 Lambda를 이용한다.

AWS Lambda의 실제 활용 사례? 출처: AWS Summit Seoul키노트 중

AWS Lambda의 실제 활용 사례? 출처: AWS Summit Seoul키노트 중

특히, 데이터 분석 영역에서 Lambda 사용도 두드러진다. FireEye는 Lambda를 이용하여 침입 탐지 시스템을 만들었는데, 기존에 맵리듀스(MapReduce) 기능을 Lambda 함수로 바꾸고, S3에 저장하는 새로운 아이디어를 내기도 했다.국내에서도 비트패킹컴퍼니가 음악 재생 시 광고 노출 데이터를 실시간으로 처리하기 위해 Lambda를 통해 Amazon Kinesis로 보내고, 이를 S3에 저장하거나Amazon Elasticsearch Service와 Kibana를 통해 분석 대시 보드를 만드는 서버가전혀없는원스톱분석서비스를 만들어 발표하기도 했다.

향후서버리스 아키텍처를 위한 생태계에서 필요한 것은 매우 많다. 클라우드 함수에 대한 지속적인 통합 및 배포(CI/CD) 지원, IDE 플러그인, 테스트 프레임워크는 가장 필수적이다. React 같은 현대적 웹 앱 프레임워크와의 연동 및 원활한 동영상 및 파일 처리, 사물 인터넷과의 연동, 이를 엔터프라이즈급 업무에서도 활용할 수 있는 다양한 사례를 발굴하는 것 역시 중요한 과제다.

마지막으로 무엇 보다 중요한 것은 개발자들의호기심이다. 항상 성공하는 기술은 낮은 진입 장벽에서, 호기심을 가진 기술 관심자들의참여로 이루어진다. 과거 모바일앱생태계 초기를 돌아보면, 개발자가 부업으로 만든 앱들이 대박을 친 경우가 많았다. 서버리스 아키텍처도 과거 수많은 고민을 해야했던 많은 장벽을 없애 줌으로써새로운아이디어를 시작해 볼 수 있고, 성공도 예측해 볼 수 있다. 누가 아는가? 내가 만든 작은 API가 유료로도 서비스할 수 있는 대박 서비스가 될지…

기업에서도 복잡한 문제 해결에 대한 가장 단순한 해법을 찾고, 기존 레거시를 혁신하기 위해 이를 직접 만들어 보는 개발자와 기업에게 미래가 있다. 만약 이를적용 하면 회사의 기존 사업이 망할 것 같고, 나의 일이 없어지는 내부적인 파괴(Disruption)를 일으킬 것 같은 기술처럼 보이는가? 서버리스 아키텍처를 바라보는 IT개발자의 우려와 벤더의 시각도 이와 다르지 않다.그렇다면 지금 당장 시도해야 한다.“미래는 이미 가까이에 와 있다. 다만 널리 퍼지지 않았을 뿐(The future is already here ? it's just not very evenly distributed. ?윌리암 깁슨)”이라는 말을 다시 새겨볼 때다.

*본 칼럼 내용은 본지 편집방향과 다를 수 있습니다.

칼럼니스트 : 윤석찬

약력

윤석찬 아마존웹서비스테크에반젤리스트| 1996년 웹 개발자로 인터넷 업계에 투신해 나인포유 CTO, 모질라(Mozilla) 오픈소스 커뮤니티 리더, IT 분야 블로거 등 다양한 역할을 수행해 왔다. 최근까지 다음카카오에서 연구개발 부서 리더 및 오픈 API 플랫폼 에반젤리스트로서 내부 API 플랫폼 구축과 외부 개발자 지원을 담당한 바 있다. 


http://bcho.tistory.com/1115

분산 로그 수집기 Fluentd 소개


조대협 (http://bcho.tistory.com)



요즘 들어 빅데이타 분석 관련 기술들을 보다보니, 역시나 여러 데이타 소스에서 데이타를 수집해 오는 부분이 여러 데이타 소스를 커버해야 하고, 분산된 여러 서버에서 데이타를 수집해야 하는 만큼 수집 컴포넌트의 중요성이 점점 더 올라가는 것 같다.

그래서 요즘 빅데이타를 위한 데이타(및 로그) 수집 플랫폼을 보고 있는데, 예전 Flume 등 여러 로그 수집 솔루션이 있었는 것에 비해서 조금 정리된 느낌이라고나 할까?  Scribed, Fluentd 그리고 ELK (Elastic Search + Logstash + Kibana 조합)에서 사용되는 Logstash등이 있는데, 대부분 Fluentd와 Logstash로 수렴 되는 것 같다. 양쪽 모두 오픈소스이고 별도의 엔터프라이즈 라이센스 정책을 가지고 있다.

Logstash는 아키텍쳐 적응에 대한 유연성과 연동 솔루션에 대한 호환성을 강조하고 있기 때문에 타 솔루션과 연동이 강하고 반면, Fluentd는 아키텍쳐의 단순성과 이를 기반으로 한 안정성을 초점을 두고 있다. 그래서 아키텍쳐 구성이나 설정이 간단하다.


이 글에서는 Fluentd에 대한 간략한 개념과 사용 방법에 대해서 알아보도록 하겠다.

Fluentd를 이용한 로그 수집 아키텍쳐

Fluentd를 이용한 로그 수집 아키텍쳐를 살펴보면 다음과 같다.

아래 그림과 같이 각 서버에, Fluentd를 설치하면, 서버에서 기동되고 있는 서버(또는 애플리케이션)에서 로그를 수집해서 중앙 로그 저장소 (Log Store)로 전송 하는 방식이다.




위의 그림은 가장 기본적인 구조로 Fluentd가 로그 수집 에이전트 역할만을 하는 구조인데, 이에 더해서 다음과 같이 각 서버에서 Fluentd에서 수집한 로그를 다른 Fluentd로 보내서 이 Fluentd가 최종적으로 로그 저장소에 저장하도록 할 수 도 있다.


중간에 fluentd를 넣는 이유는, 이 fluentd가 앞에서 들어오는 로그들을 수집해서 로그 저장소에 넣기 전에 로그 트래픽을 Throttling (속도 조절)을 해서 로그 저장소의 용량에 맞게 트래픽을 조정을 할 수 있다.

또는 다음 그림과 같이 로그를 여러개의 저장소에 복제해서 저장하거나 로그의 종류에 따라서 각각 다른 로그 저장소로 라우팅이 가능하다.


Fluentd 내부 구조

Fluentd를 이용해서 로그 수집 아키텍쳐를 구성하는 방법을 대략적으로 알아보았는데, 그렇다면 Fluentd 자체의 구조는 어떻게 되어 있을까?

Fluentd는 크게 다음 그림과 같이 Input,Parser,Engine,Filter,Buffer,Ouput,Formatter 7개의 컴포넌트로 구성이 된다.  7개의 컴포넌트중 Engine을 제외한 나머지 6개는 플러그인 형태로 제공이 되서 사용자가 설정이 가능하다.

일반적인 데이타 흐름은 Input → Engine → Output 의 흐름으로 이루어 지고,  Parser, Buffer, Filter, Formatter 등은 설정에 따라서 선택적으로 추가 또는 삭제할 수 있다.



Input

Input은 로그를 수집하는 플러그인으로, 다양한 로그 소스를 지원한다. HTTP, tail, TCP 등 기본 플러그인 이외에도, 확장 플러그인을 통해서 다양한 서버나 애플리케이션으로 부터 다양한 포맷의 데이타를 수집할 수 있도록 해준다.

Parser (Optional)

Input 플러그인을 통해서 데이타를 읽어도 데이타 포맷이 Fluentd에서 지원하지 않는 데이타 포맷인 경우가 있기 때문에, 이 데이타를 파싱 하기 위해서, Parser 플러그인을 선택적으로 사용할 수 있다. Regular expression  기반으로 스트링을 Parsing 하는 플러그인 뿐 아니라, apache, nginx, syslog등 다양한 포맷의 데이타를 파싱할 수 있는 플러그인을 제공한다.

Filter (Optional)

Filter 플러그인을 읽어드린 데이타를 output으로 보내기 전에, 다음과 같은 3가지 기능을 한다.

  • 필터링

  • 데이타 필드 추가

  • 데이타 필드 삭제 또는 특정 필드 마스킹

필터링은 특정 데이타만 output 필드로 보내고, 나머지는 버리도록 한다. 예를 들어 로그 데이타에 “seoul”이라는 문자열이 있을 경우만 로그 서버로 보내거나 “error”, “warning”과 같은 특정 패턴이 있을 경우에만 로그 저장소로 보내도록할 수 있다.


데이타 필드 추가는 기존 들어온 로그 데이타에 데이타를 전송한 서버명 (Host명)등을 추가해서 로그 저장소로 보낼 수 있다.

마지막으로 데이타 필드 삭제는 불필요한 필드를 삭제하거나 개인 정보등 민감 정보를 삭제하거나 해쉬화하여 데이타 저장소로 보낼 수 있는 기능을 한다.

Output

Output은 Input 플러그인과 반대로, 앞에서 필터링된 데이타를  데이타 저장소 솔루션에 데이타를 저장하도록 한다. (mongodb나 AWS S3 , Google의 Big query등)

Formatter (Optional)

Output 플러그인을 통해서 데이타를 저장소에 쓸 때, Formatter 를 이용하면 쓰는 데이타의 포맷을 정의할 수 있다.(cf. Input의 parser가 포맷에 맞게 읽는 플러그인이라면, Formatter는 Output을 위한 포맷을 지정하는 플러그인이라고 보면 된다.)

Buffer (Optional)

Input에서 들어온 데이타를 바로 Output으로 보내서 쓰는것이 아니라 중간에 선택적으로 Buffer를 둬서 Throttling을 할 수 있다. 버퍼는 File과  Memory 두가지를 사용할 수 있다.

간단하게 구조와 작동 원리를 보면 다음과 같다.



<그림. fluentd의 로그 writing 흐름>

원본  http://docs.fluentd.org/articles/buffer-plugin-overview


버퍼에는 로그데이타를 분리하는 tag 단위로 chunk가 생성이 된다.

chunk는 태그별 큐라고 보면 된다. 예를 들어 error, info, warning, user 와 같이 태그를 분리하면 error 로그는 error chunk에 저장이 되고, info 로그는 info chunk에 저장된다.

Chunk에 데이타가 쌓여서 buffer_chunk_limit 만큼 chunk가 쌓여서 full이 되거나, 또는 설정값에 정의된 flush_interval 주기가 되면 로그 저장소로 로그를 쓰기 위해서 Queue에 전달이 된다.


<그림. Memory buffer 설정 예제>

참고 : http://docs.fluentd.org/articles/buffer-plugin-overview


다음 Queue에서는 데이타를 읽어서 로그 저장소에 데이타를 쓰는데, 로그 저장소에 문제가 없다면 바로 로그가 써지겠지만, 네트워크 에러나 로그 저장소 에러로 로그를 쓰지 못할때는 retry_wait 시간 만큼 대기를 한 후에, 다시 쓰기를 시도한다. 다시 쓰기를 실패하면 전에 기다린 시간의 2배 만큼, 또 실패하면 또 2배만큼을 기다린다. (1초, 2초, 4초,8초…) 다시 쓰기 시도는 설정값에 지정된 retry_limit 횟수까지 계속 진행한다.

만약에 Queue 가 차버렸을때 처리에 대한 정책을 설정할 수 있는데, “exception”과, “block” 모드 두가지고 있고, exception 모드일 경우에는 BufferQueueLimitError 를 내도록 하고, block 모드의 경우에는 BufferQueueLimitError가 해결될때 까지, input plugin을 중지 시킨다 (더이상 로그를 수집하지 않는다는 이야기).

Queue가 차버렸을때 다른 처리 방법으로는 큐가 다 찾을때, Sencondary output을 지정해서, 다른 로그 저장소에 로그를 저장하는 방법이 있다. 예를 들어 로그를 mongodb에 저장하도록 했는데, mongodb 나 네트워크 장애로 로그를 쓸 수 없는 경우에는 secondary output을 AWS S3로 지정해놓고, S3로 로그를 일단 저장하게 하고 나중에 mongodb가 복구된 후에, S3에서 다시 mongodb로 로그를 수집하는 방식을 취할 수 있다.



<그림. Secondary output 설정 예제>

출처 : http://docs.fluentd.org/articles/buffer-plugin-overview


Buffer 플러그인과, 에러 처리에 대한 자세한 내용은 http://docs.fluentd.org/articles/buffer-plugin-overview 를 참고하기 바란다.


데이타 구조

다음으로 Fluentd가 내부적으로 어떻게 로그 데이타를 핸들링 하는지 데이타 구조를 살펴보면 다음과 같다.



출처 :http://pt.slideshare.net/frsyuki/fluentd-set-up-once-collect-more


데이타는 크게 3가지 파트로 구성된다. Time, tag, record

  • Time : 로그데이타의 생성 시간

  • Record : 로그 데이타의 내용으로 JSON형태로 정의된다.

  • Tag : 이게 가장 중요한데, 데이타의 분류이다. 각 로그 레코드는 tag를 통해서 로그의 종류가 정해지는데, 이 tag에 따라서 로그에 대한 필터링,라우팅과 같은 플러그인이 적용 된다.

간단한 테스트

테스트 환경은 맥북을 기준으로 하였다.

http://docs.fluentd.org/articles/install-by-dmg 를 따라서 테스트를 하면 되는데, 먼저 fluentd를 받아서 인스톨을 한다.

인스톨이 끝나면, fluentd 프로세스인 td-agent는 /opt/td-agent/usr/sbin/에 인스톨이 된다.

그리고 디폴트 설정 파일은 /etc/td-agent/td-agent.conf에 저장된다.

td-agent.conf의 내용을 보면 다음과 같다.



<ROOT>

 <match td.*.*>

   type tdlog

   apikey xxxxxx

   auto_create_table

   buffer_type file

   buffer_path /var/log/td-agent/buffer/td

   <secondary>

     type file

     path /var/log/td-agent/failed_records

     buffer_path /var/log/td-agent/failed_records.*

   </secondary>

 </match>

 <match debug.**>

   type stdout

 </match>

 <source>

   type forward

 </source>

 <source>

   type http

   port 8888

 </source>

 <source>

   type debug_agent

   bind 127.0.0.1

   port 24230

 </source>

</ROOT>



<source> 부분을 보면 type이 http, port가 8888인 정의가 있다. 이 정의는 http://localhost:8888 로 부터 로그를 수집하겠다는 정의이다.

다음 <match>부분을 보면 <match debug.**> 라는 정의로 태그가 debug.**  로 정의된 로그에 대해서 type stdout으로, stdout (화면)으로 바로 출력하겠다는 정의이다.

http://localhost:8888/{debug.**} 로 들어오는 요청에 대해서 stdout으로 로그를 출력하겠다는 설정이다.


설정 파일을 확인했으면, 이제 기동을 해보자

/opt/td-agent/usr/sbin 디렉토리에서 -c 옵션으로 설정 파일을 지정하고 td-agent를 다음과 같이 실행해보자

% ./td-agent -c /etc/td-agent/td-agent.conf


에이전트가 실행되었으면 curl 명령을 이용하여 http://localhost:8888/debug.test 로 {"json":"message"} 로그 문자열을 전송해보자

% curl -X POST -d 'json={"json":"message"}' http://localhost:8888/debug.test


다음은 실행 결과 이다.





다음과 같이 td-agent가 기동된 후에,  맨 아랫줄에 debug.test 라는 태그 이름으로 {“json”:”message”}라는 로그가 수집되어 출력된것을 볼 수 있다.

데몬으로 실행하기

앞에서는 CLI상에서 foreground로 실행을 하였는데, 맥에서 서비스로 백그라운드 작업으로 실행을 할 수 있다. 실행 방법은

%sudo launchctl load /Library/LaunchDaemons/td-agent.plist

를 실행하면 백그라운드로 실행된다. 백그라운드로 실행을 위한 스크립트인 td-agent.plist는 fluentd설치시  /Library/LaunchDaemons/td-agent.plist에 자동 생성된다.

백그라운드 작업이기 때문에, stdout이 없고 stdout으로 출력되는 로그는 /var/log/td-agent/td-agent.log로 확인할 수 있다.





실행중인 프로세스를 종료 하는 방법은

%sudo launchctl unload /Library/LaunchDaemons/td-agent.plist

를 사용하면 된다.


다음 글에는 실제로 fluentd 를 설정해서 Google의 Bigquery또는 큐로 로그를 전달하는 설정 방법에 대해서 알아보겠다.


쉽게 풀어쓴 딥러닝(Deep Learning)의 거의 모든 것

요즘 딥 러닝(Deep Learning)이 핫합니다. 몇 년전부터 기계학습(Machine Learning)이 일반인들에게 알려지기 시작하더니, 지금은 기계학습의 한 종류인 딥 러닝이 아예 기계학습이란 단어를 대체할 듯한 기세인 듯 합니다. 특히 구글이 딥 러닝 전문가 기업인 딥 마인드(Deep Mind)를 인수하고, 페이스북이 딥 러닝 대가인 뉴욕대학의 얀 러쿤(Yann LeCun) 교수를 인공지능 센터장으로 모셔갔으며, 중국의 구글이라 불리는 바이두에서도 기계학습 분야의 스타 학자 스탠포드 대학의 앤드류 응(Andrew Ng) 교수를 모셔가는 등, 지금은 바야흐로 딥러닝 인재전쟁에 가까운 모습입니다.

페이스북 인공지능 연구소 수장, 얀 러쿤(Yann LeCun) 교수 (사진출처)

그렇다면 딥 러닝이란 과연 무엇일까요? 오늘은 딥 러닝의 전반적인 개념에 대해 거칠게 한번 훑어보도록 하겠습니다.
(업데이트) 이 글의 후속편인 Convolutional Neural Network에 대한 이해가 업데이트 되었습니다. 이 글을 읽으신 후 꼭 한번 읽어보세요!

출처 : Terry's Facebook, https://goo.gl/Yo3Tvi

딥 러닝은 사실 새로운 개념이 아닙니다. 오래전부터 있어오던 인공신경망(Artificial Neural Network, ANN)과 크게 다를 바 없죠. '인공신경망'이라고 하면 단어에서 나오는 뽀대(?) 때문인지 막 복잡한 뇌 구조가 생각하면서 꿈 같은 이야기가 펼쳐질 것 같은 느낌 드는데요, 사실 인공신경망은 그렇게 판타스틱한 개념은 아닙니다.

그저 선형 맞춤 (linear fitting)과 비선형 변환 (nonlinear transformation or activation)을 반복해 쌓아올린 구조에 불과하죠. 다시 말해, 인공신경망은 데이터를 잘 구분할 수 있는 선들을 긋고 이 공간들을 잘 왜곡해 합하는 것을 반복하는 구조라고 할 수 있습니다. 선 긋고, 구기고, 합하고, 선 긋고, 구기고, 합하고, 선 긋고, 구기고, 합하고...(먹고 뜯고 맛보고 즐기고...-_-..)

파란선과 빨간선의 영역을 구분한다고 생각해보자. 그냥 구분 선을 긋는다면 아마 왼쪽처럼 불완전하게 그을 수 있을 것이다. 하지만 공간을 왜곡하면 오른쪽 같이 아름답게 구분선을 그릴 수 있다. 이처럼 인공신경망은 선 긋고, 구기고, 합하고를 반복하여 데이터를 처리한다. (사진출처: colah's blog)

예를 들어 컴퓨터가 사진 속에서 고양이를 검출해내야 한다고 생각해보죠. '고양이'라는 추상적 이미지는 아마 선, 면, 형상, 색깔, 크기 등 다양한 요소들이 조합된 결과물일 것입니다. 이것은 아마 '선 30cm 이상은 고양이, 이하는 고양이 아님', 또는 '갈색은 고양이, 빨간색은 고양이 아님' 처럼 간단한 선형 구분으로는 식별해 낼 수 없는 문제겠죠. 딥러닝은 이 과제를 선 긋고 왜곡하고 합하고를 반복하며 복잡한 공간 속에서의 최적의 구분선을 만들어 내는 목적을 가지고 있습니다.(1)

그럼 어떠한 규칙으로 선을 긋고 공간을 왜곡하냐고요? 바로 데이터에 근거하는 거죠. 일단 대충 선을 긋고 그것들을 살살살살 움직여가며 구분 결과가 더 좋게 나오도록 선을 움직이는 겁니다. 이러한 과정을 최적화(optimization)이라고 하는데요, 딥러닝은 아주 많은 데이터아주 오랜 시간의 최적화를 통해 데이터를 학습합니다. 양에는 장사 없다고나 할까요?

여러 개의 뉴런(선형 맞춤 + 비선형 변환)이 합쳐지면 복잡한 형상의 함수도 추정할 수 있다. (사진출처)

사실 인공신경망은 1940년대에 이미 개발된 방법이었고, 1980년대에 역전파(back propagation) 방법이라는 최적화 방법이 소개되며 인공신경망 연구가 절정기애 이른 바 있습니다. 이후 인공신경망은 영상처리, 인공지능, 제어 등 다양한 분야에 적용 되었는데요, 90년대에 이르러 그 연구가 포화 상태에 이르고, 이내 한계가 보이기 시작하더니 곧 암흑기를 만나게 됩니다. 심지어 2000년대 초반 논문 심사에서는 '인공신경망'이란 단어만 나오면 '뭐야, 이거 옛날거자나?'라며 리젝을 하기도 했었으니까요. 그렇게 인공신경망은 사라져 갔고, 2000년 대에는 비선형 함수를 이용한 다양한 커널 방법(e.g. Support Vector Machine, Gaussian Process)들이 기계학습의 대세를 이루게 됩니다.

딥 러닝의 일등 공신, 토론토 대학의 힌톤 교수 (사진출처: 토론토대학)

모두가 인공신경망을 외면하던 암흑기 시절, 그래도 꿋꿋하게 인공신경망 외길을 걸어오던 학자가 있었으니 바로 그가 딥러닝의 일등 공신, 토론토 대학의 제프리 힌톤(Geoffrey Hinton) 교수입니다. 인공신경망이 외면받는 여러 한계들 중 대표적인 문제는 바로 최적화가 쉽지 않다는 점이었습니다. 생각해보세요. 수 만개의 뉴론들이 수 백만개의 선들에 의해 연결되어 있고 여러분들은 이 선들에 적당한 값들을 할당해야 합니다. (일명 parameter training이죠.)

이걸 최적화 알고리즘을 통해 해줘야 하는데, 최적화 알고리즘이 만약 진짜 최적값이 아닌 잘못된 최적값에 도달하면 어쩌죠? 예를 들어 최고 높은 산봉오리에 올라가야 하는게 목적이라고 하면, 앞만 보고 막 달려서 산 봉우리에 올랐더니 '엥? 이 산이 아닌게벼...?'라고 하면 어쩌냔 말입니다. 인공신경망은 그 구조가 워낙 복잡했기에 이런 문제가 발생했을 때 그야 말로 속수무책이었죠. (그래서 제 예전 지도교수님은 인공신경망을 'black magic'이라고도 하셨으니까요ㅎㅎ)

하지만 힌톤 교수는 이러한 함정(local minima)들을 데이터의 전처리과정(pre-training)을 통해 크게 해결할 수 있음을 밝혔습니다. 이 연구가 바로 "A fast learning algorithm for deep belief nets"라는 2006년의 논문인데요, 힌톤 교수는 이 논문을 통해 인공신경망의 각 층들을 먼저 비지도학습 방법(unsupervised learning)을 통해 잘 손질해주고, 그렇게 전처리한 데이터를 여러 층 쌓아올려 인공신경망 최적화를 수행하면 '이 산이 아닌게벼?' 없이 훌륭한 결과를 만들어 낼 수 있다는 것을 보였습니다.

출처 : Terry's Facebook, https://goo.gl/Yo3Tvi

이 논문을 기점으로 인공신경망 연구는 새 전기가 열리게 됩니다. 특히 인공신경망은 빅데이터와 찰떡궁합이었죠. 2006년 이전의 많은 연구들이 데이터에 대한 구체적 형상 파악에 그 노력을 쏟았었다면, 이젠 그냥 어마어마한 구조의 인공신경망엄청난 데이터를 막 때려 넣는겁니다. 그리고선 2006년 이후 개발된 세련된 최적화 기법을 써서 몇날 며칠을 학습하면 '짜잔~'하고 최고의 결과를 내놓는다는 거죠. 딥러닝 기법은 이 후 각종 머신러닝 대회의 우승을 휩쓸며 (그것도 압도적인 성능으로...) 자신이 유아독존의 기법임을 과시했고, 현재는 다른 기계학습 방법을 통해 영상처리, 음성인식 등을 연구하셨던 분들 역시 딥러닝으로 대동단결하는 양상을 보이고 있습니다.

기계학습 관련 기업들. 이 중 Facebook, Google, Baidu 등은 모두 딥러닝에 사활을 걸고 있다. (사진출처)

그렇다면 그토록 오랜 암흑기였던 인공신경망을 성공적인 딥러닝으로 환골탈태하게 한 요인은 뭘까요? 그 요인에는 크게 다음과 같은 네 가지를 꼽을 수 있습니다.

1. Unsupervised Learning을 이용한 Pre-training

앞서 힌톤 교수가 2006년에 제안했던 것이 바로 이 방법입니다. Unsupervised learning이라고 하면 (대충 말해서) '이건 사과', '이건 고양이', '이건 사람' 이런 "가르침" 없이 그냥 사과, 고양이, 사람을 다 던져놓고 구분하라고 시키는 학습 방법인데요, 그렇게되면 아무래도 컴퓨터는 비슷한 것끼리 군집(cluster)을 찾게 되겠죠. 알고리즘은 군집화하는 과정 속에서 특이한 놈들은 과감하게 개무시(;;), 결과적으로 노이즈 감소의 효과를 얻게 됩니다. 이렇게 unsupervised learning 방법으로 데이터를 고르게 잘 손질할 수 있고, 이것을 깊은 인공신경망(=딥러닝망)에 넣으면 앞서 제기한 함정들에 훨씬 적게 빠진다는 것입니다. 이것이 바로 딥러닝의 최초 진일보였죠.

2. Convolutional Neural Network의 진화

기계학습은 data→knowledge 로 바로 학습을 진행할 수도 있지만 보통 중간 단계인 특징 추출(feature extraction)을 거쳐 data→feature→knowledge 의 단계로 학습하는 것이 보통입니다. 예를 들어 사진 속에서 사물을 인식하기 위해 픽셀 값에서 먼저 특징적인 선이나 특징적인 색 분포 등을 먼저 추출한 후 이를 기반으로 '이건 사과다' '이건 바나나다'라는 판단을 내리는 것이죠. 이러한 중간 표현단계를 특징 지도 (feature map)이라고 하는데요, 기계학습의 성능은 얼만큼 좋은 특징들을 뽑아내느냐에 따라 그 성능이 매우 크게 좌지우지 됩니다. (이는 이미지 처리 뿐만 아니라 음성 인식, 자연어 분석 등 대부분의 기계학습에 적용되는 이야기입니다.)

원본 이미지(우측)와 convolutional network에 의해 추출된 특징 지도(좌측) (출처: M. Zeiler)

딥러닝의 성공 요인 중 하나를 꼽자면, 예전엔 사람의 예측에 의해 뽑히던 이 특징들을 지금은 이 마저도 기계학습을 이용해 뽑는다는 것입니다. 다시 말해, 예전엔 '선들을 추출해서 학습시키면 사물인식이 잘 될거야'와 같이 사람이 먼저 이 선들을 추출하는 알고리즘을 만들어 주었는데, 이제는 특징 추출과 학습 모두가 딥러닝 알고리즘 안에 포함되었다는 것이죠. 다단계로 특징을 추출해 학습하는 Convolutional Neural Network은 현재 딥러닝의 대세로서 특히 이미지 인식에서 큰 발전을 이룩하고 있습니다.

3. 시계열 데이터를 위한 Recurrent Neural Network

딥러닝 알고리즘을 크게 세 분류로 나누자 대략적으로 다음과 같이 나눌 수 있습니다.
- Unsupervised Learning을 기반으로 한 방법
  (e.g., Deep Belief Network, Deep Auto-encoder)
- Convolutional Neural Network의 다양한 변형들
- 시계열 데이터를 위한 Recurrent Neural Network와 게이트 유닛들
  (e.g. Long-Short Term Memory (LSTM))
시계열 데이터(Time-series data)란 시간의 흐름에 따라 변하는 데이터를 말하는데요, 예를 들면 주가도 시간에 따라 변하고, 사람의 움직임도 시간에 따라 변하고, 비디오도 시간에 따라 변하고... 이러한 시계열 데이터에서 탁월한 성능을 보여주는 딥러닝 방법이 바로 Recurrent Neural Network (RNN) 입니다. RNN은 매 순간마다의 인공신경망 구조를 쌓아올렸다고 생각하시면 되는데요, 예를 들면 100초면 100개의 인공신경망을 쌓아올린거죠. (그래서 딥러닝 중에 가장 깊은 구조라고도 불립니다.)

예전의 RNN은 인공신경망이 너무 깊어서 오랜 시간 전의 데이터들을 까먹는 현상(vanishing gradient problem) 때문에 학습이 힘들었는데요, Jurgen Schmidhuber 교수의 Long-Short term Memory (LSTM) 이란 게이트 유닛을 각 노드마다 배치하여 이러한 문제를 극복, 현재는 Convolutional Network의 가장 강력한 경쟁 상대로 자리매김하고 있습니다.

매 순간의 인공신경망을 쌓아 올린 Recurrent Neural Network (사진출처)

4. GPU 병렬 컴퓨팅의 등장과 학습 방법의 진보

사실 예전엔 '많은 데이터로 가지고 이렇게 저렇게 하면 아마 잘 될거야...'라는 생각들은 가지고 있더라도 그것을 구현하는 것이 쉽지 않았습니다. 잘 될지 안될지도 모르는데 수십 대의 컴퓨터를 몇 달간 돌리고 있을 수는 없는 노릇이니까요. 하지만 GPGPU (General-Purpose computing on Graphics Processing Units)이란 개념이 개발되며 저렴한 가격으로 CPU와 병렬처리를 할 수 있는 GPU 제품들이 출시되었고, 이를 효율적으로 이용하는 언어구조(e.g. CuDA)들이 개발되며 딥러닝은 그 컴퓨팅 시간이 수십분의 일로 줄어 들었습니다.

연구에 사용할 수 있는 데이터 풀도 많아져 예전엔 기껏해야 몇 만개의 손 글씨 데이터(e.g. MNIST)가 전부이던 것이 지금은 천 만장의 고해상도의 사진들(e.g. ImageNet)은 물론, 필요하다면 구글이나 유튜브에서 자료를 끌어올 수도 있었으니 말이죠.

그리고 인공신경망 알고리즘적인 문제로는 비선형 변환에 쓰이는 Rectified Linear Unit (ReLU)의 개발과 거대 망을 선택적으로 학습하는 Drop-out의 발견이 딥러닝의 성능을 크게 향상 시켰답니다. 이러한 잔기술(?)에 대해서도 할 얘기가 많지만 깊은 얘기는 언젠가 또 해드리도록 하죠. ('언젠가 밥 한번 먹자'와 비슷 한 얘기입니다..;;)

구글은 2012년 1000대의 컴퓨터로 1000만 개의 유튜브 이미지를 딥러닝으로 분석해 사람과 고양이를 구분해 냈다. 내게도 컴퓨터 지원 좀 해달라... (출처 : Q. Le)

지금까지 딥러닝에 대해 알아봤습니다. 요약하자면 딥러닝은 사실 오래 전부터 있어오던 인공신경망과 크게 다를 바 없지만 알고리즘적인 발전과 하드웨어의 발전, 그리고 빅데이터의 힘에 의해 현재 최고 성능을 가진 기계학습 방법으로 평가받고 있으며, 미래 인공지능의 희망으로 떠오르고 있다는 이야기였습니다.

그렇다면 딥러닝 말고 다른 기계학습 방법들은 모두 사라져야 하는 걸까요? 물론 그것은 아닙니다. 일단 딥러닝은 많은 양의 데이터많은 컴퓨팅 자원을 필요로 합니다. (저도 이번에 80만원짜리 GPU를 구매...ㅠ) 따라서 핸드폰이나 웨어러블과 같은 포터블 기기는 이러한 컴퓨팅이 불가능할테니 딥러닝을 적용하기 쉽지 않겠죠. 또한 로봇과 같이 실시간성(real-time)이 보장되어야 하는 분야 역시 다른 기계학습 방법을 취하는게 좋을 수도 있을 것입니다. (이건 마치 컴퓨터엔 윈도우, 핸드폰엔 안드로이드와 같은 맥락이라 할 수 있죠.)

하지만 그렇다고 딥러닝이 이들 분야와 무관하냐하면 꼭 그렇지만은 않습니다. 여러분이 컴퓨터가 좋아서 구글 검색 결과가 좋나요? 다 구글 서버에서 알아서 처리해주니 그런거지요. 딥러닝도 마찬가지로 만약 디바이스가 사물인터넷을 이용해 머리 좋은 서버와 잘 교신한다면 포터블 디바이스 역시 딥러닝의 은총을 받을 수 있을 것이라 생각합니다. 특히 구글이 로봇의 미래라 생각하는 클라우드 로보틱스를 구현한다면 여러 로봇이 집단 지성을 발휘하며 문제를 해결해 나가는 것을 미래에 볼 수도 있겠지요. (참고: "구글의 새 로봇 수장, 제임스 커프너는 누구인가")

딥러닝, 인공지능의 가장 희망적인 미래임은 분명합니다. 이 분야와 관계 없으시더라도 여러분도 아마 공부 좀 하셔야 할걸요? ^^ 앞서 말씀드렸듯 이 글의 후속편인 Convolutional Neural Network에 대한 이해도 꼭 한번 읽어보세요!

(1) 쉽게 말씀드리려고 제가 딥러닝과 classification 문제를 섞어서 말씀드린 건데요, 사실 딥러닝은 real-value를 다루는 regression문제에도 적용될 수 있습니다. 

* T-Robotics의 글은 facebook과 rss reader로도 받아보실 수 있습니다.


'머신러닝 > 딥러닝' 카테고리의 다른 글

Neural Network  (0) 2016.07.06
<웹진 175호 : 공학 트렌드> 인공지능 - 딥 러닝 편  (0) 2016.05.12
DeepMind moves to TensorFlow  (0) 2016.05.09

+ Recent posts