astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Spark3分クッキング HBaseで作る100万通りの文書分類器

f:id:astamuse:20171004104538j:plain

こんにちは。最近GINZA SIXで本当のスタバ*1を知ってしまった福田です。

私たちの身の周りは、様々なデータで溢れています。 ある2つの異なるデータ集合を互いに紐付けたいこともよくあります。

どのように紐付けられるでしょうか。

一方のデータ集合から分類器をつくることができれば、分類結果を媒介として他のデータ集合とのマッチングができるかもしれません。

では、どうやって分類できるでしょう。

ここではSparkとHBaseを使って実装がシンプルで、文書分類でよく使われるナイーブベイズの分類器を実装してみます。

材料と調理器具

材料

特許の要約と分類のデータ

簡単のため以下のように正規化されたテーブル構造のデータがあるとします。

f:id:astamuse:20171003142125p:plain

特許出願(appln)を親として、要約テキスト(appln_abstr)と、分類コード(appln_ipc)がぶら下がっています。今回使うのは右側の2つのデータのみです。

特許要約のデータ(appln_abstr)

約4千万行

サンプル

+--------+-------------------------------------------------------------------------------------------------------+
|appln_id|appln_abstract                                                                                         |
+--------+-------------------------------------------------------------------------------------------------------+
|153620  |The present invention relates to uses, methods and compositions for modulating replication of viruse...|
|197020  |A base station includes a group determination unit grouping mobile stations residing within a cell a...|
|286620  |The programming method comprises supplying a turnoff voltage to the source terminal of the selected... |
+--------+-------------------------------------------------------------------------------------------------------+

*2

分類コードのデータ(appln_ipc)

約2億行

サンプル

+--------+----------------+                                                     
|appln_id|ipc_class_symbol|
+--------+----------------+
|153620  |A61K  31/57     |
|153620  |A61P  31/14     |
|153620  |C12N  15/113    |
|197020  |H04L   5/22     |
|197020  |H04W   4/06     |
|197020  |H04W  72/00     |
|286620  |G11C  16/12     |
|455820  |H04L   1/18     |
|455820  |H04W  28/04     |
+--------+----------------+

*3

調理器具
  • Spark 1.6
  • HBase 1.2.0

*4

設計編

ナイーブベイズの文書分類器*5では、分類対象の文書Dについて、分類毎に事後確率P(C|D)を計算し、その確率が最大のものを選択します。

分類アルゴリズム

f:id:astamuse:20171003143222g:plain

事後確率

f:id:astamuse:20171003143311g:plain

  • P(C|D): 文書Dが与えられたときに分類Cである確率(事後確率)
  • P(C): 分類Cが現れる確率(事前確率)
  • P(D|C): 分類Cが与えられたときに文書Dが生成される確率(尤度)

P(D|C)は次の式のように、分類Cにワードが出現する確率の積で表されます。

f:id:astamuse:20171003143403g:plain

ここで、分類時に必要なデータを事前に集計、計算し、永続化したものをモデルとします。

HBaseのデータ構造はは分散ソート済みマップとも呼ばれ、疎なデータを効率よく扱うことができます。ここではその構造を活かしてテーブル設計をしました。

テーブルレイアウト(イメージ)
row-key feature: stats:
cafebabe スギ花粉 4 n_occurence 16
2 s_occurence 300
n_feature 2
s_val 6
label 花粉症対策
prior_prob 0.053
deadbeaf ウィルス 20 n_occurence 10
手洗い 3 s_occurence 300
うがい 16 n_feature 3
s_val 39
label インフルエンザ対策
prior_prob 0.033
  • row-keyはラベルのハッシュ値を分類IDとして使用
  • feature:カラムファミリ
    • ワードとその出現回数をKey-Valueとして格納
  • stats:カラムファミリ
    • prior_prob: 事前確率
    • label: 分類ラベル
    • s_val: 分類におけるの語彙毎の出現回数の合計
    • n_occurence: 分類の出現回数(デバッグ用)
    • s_occurence: 分類の総出現回数(デバッグ用)
    • n_feature: 分類の語彙数(デバッグ用)

分類に必要な値以外に調査や検証に役立つ値も格納しています

実装編

テーブルの作成

CREATE 'test_model', { NAME => 'feature', VERSIONS => 1, COMPRESSION => 'LZ4', DATA_BLOCK_ENCODING => 'FAST_DIFF', 'IN_MEMORY' =>
'true' },  
   { NAME => 'stats', VERSIONS => 1, COMPRESSION => 'LZ4', DATA_BLOCK_ENCODING => 'FAST_DIFF', 'IN_MEMORY' => 'true' },
   { SPLITS => [
     '1000000000000000000000000000000000000000',
     '2000000000000000000000000000000000000000',
     '3000000000000000000000000000000000000000',
     '4000000000000000000000000000000000000000',
     '5000000000000000000000000000000000000000',
     '6000000000000000000000000000000000000000',
     '7000000000000000000000000000000000000000',
     '8000000000000000000000000000000000000000',
     '9000000000000000000000000000000000000000',
     'a000000000000000000000000000000000000000',
     'b000000000000000000000000000000000000000',
     'c000000000000000000000000000000000000000',
     'd000000000000000000000000000000000000000',
     'e000000000000000000000000000000000000000',
     'f000000000000000000000000000000000000000'
     ] }
  • スループットを稼ぎたいのでインメモリ指定しています
  • データが分散するように分割ポイントを指定しています

モデルデータの生成

Sparkを使ってデータを変換していきます。

ステップ1 グループIDの生成

文書:分類が1:Nの関係となっているのを、ここでは複数コードの集合を1つのグループとして扱うように変換し、このグループのIDを生成します。*6

// 出願ごとに複数付与される階層分類コードの上位4桁の集合からカグループを生成し、ハッシュ値をキーとする
val appln_group = appln_ipc.groupBy("appln_id")
  .agg(concat_ws(",", sort_array(collect_set(substring($"ipc_class_symbol", 0, 4)))) as "ipc_sss",
    sha1(concat_ws(",", sort_array(collect_set(substring($"ipc_class_symbol", 0, 4))))) as "group")

DataFrame APIを使用しています。

+--------+--------------+----------------------------------------+              
|appln_id|ipc_sss       |group                                   |
+--------+--------------+----------------------------------------+
|153620  |A61K,A61P,C12N|947ed80b48cae17d652fdd8fb6f6de2eff130710|
|197020  |H04L,H04W     |fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|
|286620  |G11C          |e3a1862a8f7ce681b57c4c41f711922f3b0bb490|
|455820  |H04L,H04W     |fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|
+--------+--------------+----------------------------------------+

この段階で、1文書1行のデータに変換されます。

ステップ2 学習データの生成
// 約8割を学習データとする
val trainingset = appln_group.filter($"appln_id" % lit(5) !== lit(0)

// 学習データ(要約テキストからキーワードを抽出し、キーワード毎に行展開)
val instances = trainingset.as("ag")
  .join(appln_abstr, "appln_id")
  .select($"ag.appln_id",
    explode(Util.wordcount($"appln_abstract", lit(30))) as "wc",
    $"ag.group" as "group",
    $"ag.ipc_sss" as "ipc")
  .select($"*", $"wc"("_1").as("keyword"), $"wc"("_2").as("n"))
  .drop("wc")
}

ここはデータ全体の8割を学習データとし、残りをテストデータにし、実際の学習データを作ります。*7

要約のデータをjoinしつつキーワードを抽出し行を展開しています。 *8

データイメージ

+--------+----------------------------------------+--------------+----------------------------------------------------+---+
|appln_id|group                                   |ipc           |keyword                                             |n  |
+--------+----------------------------------------+--------------+----------------------------------------------------+---+
|50020   |947ed80b48cae17d652fdd8fb6f6de2eff130710|A61K,A61P,C12N|virus                                               |5  |
|50020   |947ed80b48cae17d652fdd8fb6f6de2eff130710|A61K,A61P,C12N|cell proliferative disorders                        |4  |
|50020   |947ed80b48cae17d652fdd8fb6f6de2eff130710|A61K,A61P,C12N|Ras-pathway                                         |2  |
|454620  |e3a1862a8f7ce681b57c4c41f711922f3b0bb490|G11C          |ground stage                                        |1  |
|454620  |e3a1862a8f7ce681b57c4c41f711922f3b0bb490|G11C          |process condition                                   |1  |
|454620  |e3a1862a8f7ce681b57c4c41f711922f3b0bb490|G11C          |TR1                                                 |1  |
+--------+----------------------------------------+--------------+----------------------------------------------------+---+

ここまでで下ごしらえが完了です。次のステップではHBaseのテーブルの各カラムファミリに流しこむデータを生成していきます。

HDFS上のステージングディレクトリに一通りHFileを出力した後、最後にファイル群をHBaseのデータディレクトリに移動させてバルクロードを完了させます。

ステップ3 HFileの生成

hbase-sparkというライブラリ*9を使用してHFileを生成していきます。ここでは入力としてRDDへの変換をしています。

def createHFile(hbaseContext: HBaseContext, tableName: TableName,
    rdd: RDD[(Array[Byte], Array[(Array[Byte], Array[Byte], Array[Byte])])], stagingDir: String) = {

  hbaseContext.bulkLoad[(Array[Byte], Array[(Array[Byte], Array[Byte], Array[Byte])])](
    rdd,
    tableName,
    (r) => {
      r._2.map { v =>  
        (new KeyFamilyQualifier(r._1, v._1, v._2), v._3)
      }.iterator
    },
    stagingDir)
  }
}


val hbconf = HBaseConfiguration.create()
val hbaseContext = new HBaseContext(sc, hbconf)

// グループ毎のキーワード出現数(feature:列ファミリ)の集計データ生成
val wc_by_group = instances.groupBy($"group", $"keyword").agg(sum("n")).orderBy($"group", $"keyword")

val feature_put_rdd = wc_by_group.rdd.map{ x =>
  (Bytes.toBytes(x.getString(0)), Array((Bytes.toBytes("feature"), Bytes.toBytes(x.getString(1)), Bytes.toBytes(x.getLong(2))))) }

createHFile(hbaseContext, TableName.valueOf(tableName), feature_put_rdd, stagingDirClassificationModel)

出力イメージ

+----------------------------------------+----------------------------------------------------+------+
|group                                   |keyword                                             |sum(n)|
+----------------------------------------+----------------------------------------------------+------+
|947ed80b48cae17d652fdd8fb6f6de2eff130710|HSV                                                 |1     |
|947ed80b48cae17d652fdd8fb6f6de2eff130710|Methods                                             |1     |
|e3a1862a8f7ce681b57c4c41f711922f3b0bb490|first output signal                                 |3     |
|e3a1862a8f7ce681b57c4c41f711922f3b0bb490|first resistor                                      |6     |
|e3a1862a8f7ce681b57c4c41f711922f3b0bb490|first transistor                                    |6     |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|terminal                                            |1     |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|traffic                                             |3     |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|traffic differentiation                             |1     |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|transfer                                            |1     |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|wireless LAN                                        |1     |
+----------------------------------------+----------------------------------------------------+------+

stats:n_featureデータの生成

// グループ毎の特徴の種類数(stats:n_feature)のデータ生成
val n_feature_by_group = wc_by_group.groupBy($"group").agg(count("*")).orderBy($"group")

val n_feature_put_rdd = n_feature_by_group.rdd.map { x =>
  (Bytes.toBytes(x.getString(0)), Array(
    (Bytes.toBytes("stats"), Bytes.toBytes("n_feature"), Bytes.toBytes(x.getLong(1))))) }

createHFile(hbaseContext, TableName.valueOf(tableName), n_feature_put_rdd, stagingDirClassificationModel)

出力イメージ

+----------------------------------------+--------+                             
|group                                   |count(1)|
+----------------------------------------+--------+
|947ed80b48cae17d652fdd8fb6f6de2eff130710|17      |
|e3a1862a8f7ce681b57c4c41f711922f3b0bb490|83      |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|70      |
+----------------------------------------+--------+

stats:s_valとlabelデータの生成

// グループ毎のキーワード出現数の合計(stats:s_val)と 可視化ラベル(stats:label)のデータ生成
val s_val_by_group = instances.groupBy($"group", $"ipc").agg(sum("n")).orderBy($"group")

val s_val_put_rdd = s_val_by_group.rdd.map{ x =>
  (Bytes.toBytes(x.getString(0)), Array(
    (Bytes.toBytes("stats"), Bytes.toBytes("s_val"), Bytes.toBytes(x.getLong(2))),
    (Bytes.toBytes("stats"), Bytes.toBytes("label"), Bytes.toBytes(x.getString(1))))) }

createHFile(hbaseContext, TableName.valueOf(tableName), s_val_put_rdd, stagingDirClassificationModel)

出力イメージ

+----------------------------------------+--------------+------+                 
|group                                   |ipc           |sum(n)|
+----------------------------------------+--------------+------+
|947ed80b48cae17d652fdd8fb6f6de2eff130710|A61K,A61P,C12N|29    |
|e3a1862a8f7ce681b57c4c41f711922f3b0bb490|G11C          |263   |
|fc38214c2f0b7f1636cc0ee9206d2023537b0dcd|H04L,H04W     |216   |
+----------------------------------------+--------------+------+

stats:prior_probのデータの生成

// グループごとの出現回数(stats:n_occurence)のデータ生成
val n_occurence_by_group = trainingset.groupBy($"group").agg(count("*")).orderBy($"group")

n_occurence_by_group.persist(StorageLevel.DISK_ONLY)

    
val s_group_occurence = trainingset.count().toDouble


val n_occurence_put_rdd = n_occurence_by_group.rdd.map{ x =>
  (Bytes.toBytes(x.getString(0)), Array(
    (Bytes.toBytes("stats"), Bytes.toBytes("prior_prob"), Bytes.toBytes(x.getLong(1) / s_group_occurence)),
    (Bytes.toBytes("stats"), Bytes.toBytes("s_occurence"), Bytes.toBytes(s_group_occurence)),
    (Bytes.toBytes("stats"), Bytes.toBytes("n_occurence"), Bytes.toBytes(x.getLong(1))))) }

createHFile(hbaseContext, TableName.valueOf(tableName), n_occurence_put_rdd, stagingDirClassificationModel)

ステージングディレクトリに出力されたHFileをHBaseのデータディレクトリに移動させてバルクロードを完了させます。

テトリス*10のようなイメージです。

// バルクロードの完了

val conn = ConnectionFactory.createConnection(hbaseContext.config)

val load = new LoadIncrementalHFiles(hbaseContext.config)
load.doBulkLoad(
  new Path(stagingDirClassificationModel),
  conn.getAdmin,
  new HTable(hbaseContext.config, TableName.valueOf(tableName)),
  conn.getRegionLocator(TableName.valueOf(tableName)))

これで完成です。約100万分類のモデルができました。

分類フェーズ

学習フェーズで生成したモデルのテーブルを読み、実際に文書の分類を行うフェーズです。

HBaseの分類モデルのテーブルは1行が1分類となっており、分類対象の文書に含まれるキーワードから、どの分類から来たのかの確率をスコアとして計算し、スコアの高いものを10件ずつ出力しています。

下にコード(抜粋)とポイントを簡単に挙げます。

// (学習時と同じデータセット)                                                                                              
val appln_group = appln_ipc.groupBy("appln_id").agg(concat_ws(",", sort_array(collect_set(substring($"ipc_class_symbol", 0, 4)))) as "ipc_sss", sha1(concat_ws(",", sort_array(collect_set(substring($"ipc_class_symbol", 0, 4))))) as "group")        
                                                                                                                                      
// 学習に使った残りの約2割からのデータをテストデータとする                                                                     
val testset = appln_group.filter($"appln_id" % lit(5) === lit(0)).limit(100000)
testset.persist(StorageLevel.DISK_ONLY)                                                                                           
                                                                                                                                      
                                                                                                                                      
val instances = testset.as("ag").join(appln_abstr, "appln_id").select($"ag.appln_id", Util.wordcount($"appln_abstract", lit(30)) as "keyword", $"ag.group" as "group", $"ag.ipc_sss" as "ipc", row_number().over(Window.partitionBy().orderBy($"appln_id")) as "row_num")                                                                                                
                                                                                                                                      
                                                                                                                                      
// 行番号とバッチサイズを使ってバッチ分割(高速化のためバッチサイズ毎に1テーブルスキャンとするため)                              
val input = instances.map{ r =>                                                                                                   
  (r.getInt(4) / batchSize, (r.getInt(0), r.getMap[String,Int](1), r.getString(2), r.getString(3)))                               
    }.groupByKey().repartition(2048)                                                                                                 
                                                                                                                                      
val hbconf = HBaseConfiguration.create()                                                                                          
val tableNameObject = TableName.valueOf(tableName)                                                                                
val table = new HTable(hbconf, tableName)                                                                                         
val conn = ConnectionFactory.createConnection(hbconf)                                                                             
val regionLocator: RegionLocator = conn.getRegionLocator(tableNameObject)                                                         
val startEndKeys: org.apache.hadoop.hbase.util.Pair[Array[Array[Byte]], Array[Array[Byte]]] = regionLocator.getStartEndKeys()     
val range = startEndKeys.getFirst().zip(startEndKeys.getSecond())                                                                 
                                                                                                                                      
// input: _1=appln_id, _2=keyword, _3=group, _4=label                                                                  
val result = input.map{ row =>                                                                                                    
                                                                                                                                  
  import scala.math.log                                                                                                           
                                                                                                                                      
  // トップN件の分類結果を保持するための入れ物を用意しておく                                                                      
  object EntryOrdering extends Ordering[(String, String, Double)] {                                                               
    def compare(a: (String, String, Double), b: (String, String, Double)) = -(a._3 compare b._3)                                  
  }                                                                                                                               
                                                                                                                                      
  // バッチ内のエントリ(出願)毎にランキングのための優先キューを初期化                                                           
  val topK = scala.collection.mutable.Map[Int, PriorityQueue[(String, String, Double)]]()                                         
                                                                                                                                      
  for (d <- row._2) {                                                                                                             
    topK.put(d._1, new PriorityQueue[(String, String, Double)]()(EntryOrdering))                                                  
  }                                                                                                                               
                                                                                                                                      
  // バッチ毎に1テーブルスキャン(レンジスキャンをシャッフルしてクエリを分散させる)                                              
  scala.util.Random.shuffle(range.toList).foreach { r =>                                                                          
    val hbconf = HBaseConfiguration.create()                                                                                      
    val table  = new HTable(hbconf, tableName)                                                                                    
                                                                                                                                      
    val scan = new Scan(r._1, r._2)                                                                                               
                                                                                                                                      
    scan.setCaching(100)                                                                                                          
                                                                                                                                      
    val scanner = table.getScanner(scan)                                                                                          
                                                                                                                                      
    for (r: Result <- scanner) {                                                                                                  
      val classificationId = Bytes.toString(r.getRow())                                                                           
                                                                                                                                      
      val classificationStats = r.getFamilyMap(Bytes.toBytes("stats"))                                                            
      val classificationFeature = r.getFamilyMap(Bytes.toBytes("feature"))                                                        
                                                                                                                                      
      // 可視化のためのラベルを復元                                                                                               
      val label = Bytes.toString(classificationStats.getOrElse(Bytes.toBytes("label"), Bytes.toBytes("N/A")))                     
                                                                                                                                      
      // バッチサイズ件数分の処理                                                                                                 
      for (d <- row._2) {                                                                                                         
        var score = log(Bytes.toDouble(classificationStats.getOrElse(Bytes.toBytes("prior_prob"), Bytes.toBytes(0.0))))           
        val features = d._2                                                                  
                                                                                                                                       
        for (f <- features) {                                                                                                     
          val keyword = f._1                                                                                                      
          val n = f._2                                                                                                            
                                                                                                                                      
          val numerator = Bytes.toLong(classificationFeature.getOrElse(Bytes.toBytes(keyword), Bytes.toBytes(0L))).toDouble + 1.0                                                                                                                                     
          val denominator = Bytes.toLong(classificationStats.getOrElse(Bytes.toBytes("s_val"), Bytes.toBytes(1L))) + 70000000.0   
                                                                                                                                      
          val wordProb: Double = numerator / denominator                                                                          
                                                                                                                                      
          score += n * log(wordProb)                                                                                              
        }                                                                                                                         
                                                                                                                                      
        val q = topK.get(d._1).get                                                                                                
        if (q.size < 10) {                                                                                                        
          q.enqueue((classificationId, label, score))                                                                             
        } else {                                                                                                                  
          val smallest = q.dequeue()                                                                                              
                                                                                                                                      
          if (score > smallest._3) {                                                                                              
            q.enqueue((classificationId, label, score))                                                                           
          } else {                                                                                                                
            q.enqueue(smallest)                                                                                                   
          }                                                                                                                       
        }                                                                                                                         
      }                                                                                                                           
    }                                                                                                                             
                                                                                                                                      
    scanner.close()                                                                                                               
    table.close()                                                                                                                 
                                                                                                                                      
  }                                                                                                                               
                                                                                                                                      
  // バッチの各行毎に計算結果を回収                                                                                               
  val z = row._2.map{ d =>                                                                                                        
    // appln_id, keyword, group, label, result                                                                                    
    (d._1, d._2, d._3, d._4, topK.get(d._1).get.dequeueAll)                                                                       
  }                                                                                                                               
  z                                                                                                                               
}.flatMap(x => x)                                                                                                                 
                                                                                                                                      
result.persist(StorageLevel.DISK_ONLY)                                                                                            
                                                         
result.map{ r =>                                                                                                                  
  val keywords = r._2.toList.sortBy(-_._2).map{case (a, b) => s"${a}:${b}"}.mkString(",")                                         
                                                                                                                                      
  val result = r._5.reverse.map(_.productIterator.mkString(":")).mkString("|")                                                                                                                                                                                          
  // appln_id, keyword, group, label, result                                                                                      
  s"${r._1}\t${keywords}\t${r._3}\t${r._4}\t${result}"                                                                            
}.saveAsTextFile("model_test.tsv")             

ポイント

  • 分類対象の件数に応じた計算リソースと時間が必要
  • テーブルスキャンが分類対象の文書数分必要なので、バッチ化してHBase側の負荷とネットワークIOを抑制
  • 上位10分類を出力するため優先度付きキューを使用
  • レンジスキャンをシャッフルして実行することでHBaseのブロックキャッシュヒット率を上げることで速度を稼ぐ

*11

実験結果

テストデータのセットから10万件を分類器にかけてみて、出力結果の評価をしました。 文書毎に返された上位10件の分類ラベルに対して、次の6通りを集計しました。

  • 最上位で正解ラベルと完全一致する率
  • 最上位で正解ラベルの構成要素の集合との共通部分が存在する率
  • 5位以内で正解ラベルと完全一致する率
  • 5位以内で正解ラベルの構成要素の集合との共通部分が存在する率
  • 10位以内で正解ラベルと完全一致する率
  • 10位以内で正解ラベルの構成要素の集合との共通部分が存在する率

それぞれ以下の数字でした。

  • 24397/100000 0.2440
  • 45486/100000 0.4549
  • 40696/100000 0.4070
  • 66264/100000 0.6626
  • 46965/100000 0.4697
  • 73903/100000 0.7390

考察と課題

  • データの設計と実装上の工夫により比較的規模の大きなデータに対してジョブを完遂することができた
  • 分類器の精度としては今ひとつな値だが、約100万の中から10個選んだものとしてみるとランダムよりは良さそう(ラベル付けが目的かマッチングが目的かで異なる解釈)というレベル
  • 以下の要素についてもうすこし考える余地がありそう
    • 分類の数と粒度 
    • テキストの分量と質
    • 特徴(キーワード)抽出の方法
    • 正解ラベルよりもよいラベルを引き当てた可能性についてはどう評価するか

まとめ

  • SparkとHBaseを使ってスケーラブルな単純ベイズの文書分類器を実装しました。
  • 数千万件の特許要約テキストと分類コードのデータから約百万通りの分類器を学習させてみました。
  • 実装面、理論面の両方で更なる改良の余地がありそうです。

*1:http://www.starbucks.co.jp/coffee/reserve/

飲み方と豆の種類と淹れ方を選ぶとバリスタが目の前で作ってくれるスタイルです。匠の技やプロセスを間近で見れるのでエンジニアの方におすすめです。

*2:テキストは長いため切り詰めて表示しています。

*3:ipc_class_symbolは国際特許分類と呼ばれる階層化された分類コードで、出願案件毎に複数付与されます http://www.wipo.int/classifications/ipc/en/

*4:Cloudera社のCDH5を使わせていただいております。歴史的理由により、Spark1.6のコードとなっています。また、CDHでは2017年9月現在、hbase-sparkがSpark2系に対応していないようです。対応を心待ちにしています。

https://www.cloudera.com/documentation/spark2/latest/topics/spark2_known_issues.html#ki_spark_on_hbase

*5:ナイーブベイズの文書分類器については、Webや書籍で多くの情報があります。 以下の記事を参考にさせていただきました。

単純ベイズ分類器 - Wikipedia

ナイーブベイズを用いたテキスト分類 - 人工知能に関する断創録

*6:これにより粒度と分類数を調節しています。今回実験のため、ヒューリスティックに約100万分類となるような変換を施しています

*7:id列の値の採番方法と分布に依存します。ここでは単純に連番を想定しています

*8:ここでは詳細を割愛していますが、wordcountはユーザ定義関数でテキストからキーワードを抽出し、スコアの高い順にキーワードと頻度のタプルのリスト(List[(String, Int)])として返してくれるモジュールを別途実装しています。

*9:hbase-spark

https://blog.cloudera.com/blog/2015/08/apache-spark-comes-to-apache-hbase-with-hbase-spark-module/ https://github.com/apache/hbase/tree/master/hbase-spark

*10:テトリス - Wikipedia

*11:HBaseのBlock Cacheヒット率

f:id:astamuse:20171003145441p:plain 実行開始後からヒット率が上昇し始め、高い値に収束しています。施策がない場合、並列実行する複数のタスクがほぼ同時に同じ領域を読むことになり、キャッシュ領域を有効に利用出来ずヒット率が低いまま推移していたため、ゆらぎを設けて出来るだけキャッシュに詰め込みました。

データドリブンな企業とは何か~アスタミューゼ流宴会術~

こんにちは 今回、開発・デザイン以外の部署からゲスト寄稿させていただくことになりました亀久です。

自己紹介の前に、まだこのブログではアスタミューゼの組織体制がどうなっているのかを明らかにしたことがなかったと思うので、そのあたり簡単にご説明しますね(2017年9月現在)

アスタミューゼには、コーポレート本部を除くと大きく分けて3つの部署があります。

テクノロジーインテリジェンス(TI)部

『未来を創る2025年の成長領域』のコンセプト作りや、自社データベースを活用した技術情報の調査・分析業務などを行うアスタミューゼの頭脳集団

事業開発部

TI部が生み出すコンセプトやデータをもとにサービス/プロダクトを展開するビジネスサイドの部署。 新規事業開発支援/人材採用支援/知的情報Webプラットフォームの3事業に分かれています。 また、広報チームもこの部署です。

開発・デザイン部

上記を可能にするのが結局ここの人たち

私の所属は事業開発部ですが、特定の事業を専門に担当しているわけではありません。 TI部が生み出したコンセプトやデータをもとに、新サービスのプロトタイプとなるコンテンツを企画し、開発・デザイン部と連携して形にし、広報と連携して外部に発信していくという部署横断的な仕事をしています。

本記事では、日々の業務の中で、どのようなデータを活用してどういったコンテンツを開発しているのか、その一部をご紹介したいと思います。

アスタミューゼのイノベーションDBと『未来を創る2025年の成長領域』

まず、アスタミューゼがどのようなデータベースを持っているのか、下記の図をご覧ください。

f:id:astamuse:20170925115807p:plain

世界80ヵ国の新事業/新技術/新製品と投資情報をイノベーションDBとして保有しており、技術データが約8000万件、クラウドファンディングをはじめとするCtoCデータが約7200万件、企業データが約730万社、グラントデータが約300万テーマに上ります。

たしかに膨大なデータではあるのですが、アスタミューゼの「秘伝のたれ」は、むしろこちらの『未来を創る2025年の成長領域』のほうだったりします。

これらは、今後特に有望と思われる事業の指針となる市場群で、TI部が各種データおよび国内外の国際会議やシンポジウム・展示会等の情報、並びに独自のネットワークによる口コミ情報等を結集・分析し、策定しています。

アスタミューゼでは、膨大なデータと投資情報をこの『未来を創る2025年の成長領域』に沿って分類することで、企業や個人が未来を把握するお手伝いをしています。

それではこのイノベーションDBと『未来を創る2025年の成長領域』を活用したコンテンツ例をご紹介します。

(コンテンツ例1)AI分野の研究テーマ別日米比較

下の2つのグラフは、日米のAI分野における研究テーマを「どのような用途で研究しているか」という切り口で分析したものです。

日本の文部科学省による科学研究費助成事業(科研費)と、アメリカ国立科学財団 (National Science Foundation; NSF)から交付される競争的研究資金プログラムに採択された研究テーマから、独自の検索定義によりAI分野における研究テーマを抽出して用途別に分類しています。

f:id:astamuse:20170925115951p:plain

f:id:astamuse:20170925120038p:plain

ざっくり言うと、日本よりもアメリカのほうが用途明確で実践的な研究が多い、ということを示すデータです。
集計方法などの詳細についてはこちらの記事をご参照ください。

AI分野の研究テーマ日米比較 「医療・ヘルスケア」はアメリカがリード、日本は「生産技術」に注力

グラントの次は、人材採用に関するデータ活用の例です。

(コンテンツ例2)成長領域別の求人倍率推移

アスタミューゼでは『未来を創る2025年の成長領域』に沿って希少人材の採用支援サイト『転職ナビ』を約400種類運営しており、有望成長市場の変動に伴って新サイトの追加や入れ替えを行っています。

ちなみに最近リリースしたサイトには、このようなものがあります。

f:id:astamuse:20170925120246j:plain

防衛技術転職ナビ

f:id:astamuse:20170925120347j:plain

金融システム・Fintech転職ナビ

こういったサイトが約400あるわけですが、これらの求人票データや登録者データを分析して成長領域別の求人倍率推移を出してみました。

f:id:astamuse:20170925120711j:plain

ざっくり言うと、成長領域における希少人材の求人倍率(17年5月時点)は最低の「農業・食品工業」領域で37.47倍、最高の「エレクトロニクス」で121.69倍(一般職業紹介の求人倍率は同時点で1.49倍)と、希少人材の希少ぶりを示すデータです。 集計方法などの詳細や、アスタミューゼの採用支援サービスについてはこちらの記事をご参照ください。

400の転職ナビで「希少人材」を採用|アスタミューゼが仕掛ける成長戦略とは?(HR NOTE)

それでは最後にもうひとつ、とあるデータを見ていただきたいと思います。

(コンテンツ例3)牽制マトリクス

こちらの表は、社内では「牽制マトリクス」と呼ばれているものです。

f:id:astamuse:20170925123427p:plain

これは特許の「牽制」情報から、自社技術を活かせる新たな領域を見つけるためのもので、縦軸に自社の技術、横軸に『未来を創る2025年の成長領域』の有望成長市場群が並べられています。 そのロジックについては当ブログの過去記事が詳しいのでよろしければご参照ください。

さて、この「牽制マトリクス」ですが、「企業」ではなく「個人」の目線で見たとき、もうひとつの側面が浮かび上がってきます。

技術やアイデアといったものは本質的に「個人」が生み出すもので、すべての技術やアイデアは「個人」に紐づいています。

つまり、この「牽制マトリクス」上にある特許/技術を持つ「個人」は、該当する有望成長市場で活躍できる可能性が高い、ということでもあるのです。

「企業」が成長領域のベンチャー企業に投資するように、「個人」もまた自分という資本を成長領域に投入するようになるのではないか、とか。

「企業」と「個人」が同じ成長領域に投資しようとするとき、両者のアイデンティティが限りなく近づく瞬間があるのではないか、とか。

そういったことを考えながら今日もこつこつSQLを叩いています。
現場からは以上です。それでは!

デザインの良し悪しを語るのに必要な「分解」について【書体編】

前置き(長い)

先日、というか結構まえの話になるのですが、とあるSNSを閲覧していたところ「明朝体で喋る人が好きなのだけれど大体の人がゴシック体で会話をしておr…(以下省略)」といった内容を発信している方をお見かけました。
こちらの投稿は「その感じ、とてもよくわかります」と言う分かる派と「明朝体やゴシック体といったものは文字として表されるものにも関わらず、それを用いて喋るとは一体どういうことなのか?」と言った分からない派が発生し、書体について様々な意見が投じられていたようです。
しかし「分かる派」の方々でも具体的な説明を求められると、「そいつはなかなか難しい」という様子で双方の理解は平行線を辿り、霧散、といった具合です。

筆者はいちおうデザイン的なそれを生業としており、先の投稿にあったような「明朝体」「ゴシック体」という類のものを意識する時間が非デザイナーの人よりは相対的に多いような気が何となく、しなくもないかな、と感じています。 そういった事情もあり「明朝体で話す感じ」については一定の理解が可能であるのと同時に、「意味がわからない」とおっしゃる方の言い分も確かによく分かります。 なぜならこういったやり取りは書体に限ったことではなく、主にデザイン界隈、すなわち「意匠」について語られる時によく見かける風景で、昨日今日に始まったことではないからです。

例えて言うなら、ある意匠(デザイン)を見て「うわぁ、これめっちゃイケてる、最先端、それでいてダウナー」的な感想を持ったとしても他の人が「全くそうは思わない、なぜそう感じるのか説明しろ」と言われて具体的な説明ができなければ先ほどの事例と同じことで、お互いの意見はいつまでたっても平行線、一生お互いの良いと思った素敵なデザインについて理解しあえないままで終わることになり、とても悲しい、的なことになりかねない。

とまあ極端な例ですけど、でもデザインが醸し出すイメージ・印象を具体的な言葉に変換して伝えることはそんなに難解なことなのか?といったら案外そうでもないハズです。 それはデザインされたものを「分解する」ことで誰でも簡単にできることだと思います。

ひっじょーに長くなりましたが、取りあえずそういうことで今回はデザインを「分解する」的なものが何なのかについて、「書体」を例に考えていきたいと思います。

基礎を知る

まず、はじめに分解しようと思う要素の基礎を確認する必要があります。 筆者自身がよく行う事ですが、既に知っている事であっても分解していこうと要素については調べ直すようにしています。 知っていると思っていた事柄も過大解釈していたり、思い違いをしていたり、偏った見方をしていたり、という場合も決してゼロではありません。 基本的には自分の知識や記憶をあてにしない、というスタンスでいることが大事なことかもしれません。

ということで、まずは書体の基本をざっと確認していきます。

書体の基本

書体とフォント

書体の英訳がフォントと考えている人もいるため混同されがちですが、厳密には違います。

書体:統一されたデザインを持つ文字の集まり
フォント:書体をディスプレイ表示や印刷などで使えるようにデータ化したもの

このような違いがあるものの実際には「書体」のことを指すときにも「フォント」と言いがちですし、仕事以外の場ではその違いを意識して使うことはあまりないかもしれません。 ちなみに書体の英訳として相当するのはtypefaceらしいです。

和文書体と欧文書体

そして「書体」と一口に言っても世界には数え切れないほどの、ありとあらゆる種類が存在しています。 すべてを確認するのはこの場ではムリな気がする。 なので理解しやすいように大きく2つに分けてみるとまずは「和文書体」と「欧文書体」に大別することができます。 読んで字のごとくですが、

  • 和文書体:日本語(ひらがな/カタカナ/漢字/その他)をデザインした書体

  • 欧文書体:アルファベット表記の文字をデザインした書体

簡単に説明するとこのようになります。

明朝体とゴシック体

さらに和文書体も大きく2つの種類に分けることができるのですが、それが先述した「明朝体」と「ゴシック体」です。 これらは馴染みがある書体ですよね。 それぞれの特徴は

  • 明朝体:縦の線は太く、横の線は細くデザインされており、筆で書いたような「うろこ(※)」「はね」「はらい」などもデザインされた書体

  • ゴシック体:線の太さがほぼ均等で直線的にデザインされた書体

(※)うろこ:線の右端、曲がり角の右肩につけられる三角形のこと。毛筆で書いた文字にある「押さえ」。

f:id:astamuse:20170920112706p:plain

セリフ体とサンセリフ体

また、欧文書体も「セリフ体」と「サンセリフ体」の2つに大別できます。

  • セリフ体:縦線が細く、セリフ(※)をもつ書体。欧文書体の中でもっともスタンダード。

  • サンセリフ体:すべての線の太さがほぼ均等で、セリフをもたない書体

(※)セリフ:明朝体でいう「うろこ」と同じ(ような)もの

f:id:astamuse:20170920113005p:plain

こうやって比べてみると、和文書体も欧文書体も同じような大別の仕方で、「あしらい」なども非常に似ていることがわかります。

と、ここまでの解説は書籍やwebなどにももっと詳しく解説されていますのでもっと知りたい方は自力で調べて下さい。

「書体」とはいったい何なのか(「分解」的なことの実践)

それでは「書体」がどういったものか大まかにわかったところで、実践です。

書体」とは上記の『書体の基本』にも書いたように【統一されたデザインを持つ文字の集まり】です。 ではその「文字」とは何か? 端的に言ってしまえば「言葉/言語」を表す「図形」と言い換えられます。

ではその「図形」は何なのか? それは「点と線」で構成された形です。

つまり、人はある特定の「点と線」の重なりを「文字」と認識しているわけです。

ではここで、2つの線を見比べてください。

f:id:astamuse:20170920113028p:plain

「力強い線」はどちらで「繊細な線」はどちらでしょう?
通常、左の線を力強く、右の線を繊細と感じるのではないでしょうか。
このように、人は「線」の太さ・強弱に対して共通する印象・イメージを持っていると言えます。

では

  • 「線(と点)」の重なりを「文字」を認識していて

  • その「線」の強弱に特定の共通するイメージを持っている

ということは 持ってもらいたい印象・イメージを、線に強弱をつけることによってコントロールされた「文字」が「書体」ということになります。

便宜的に線の強弱と書きましたが、もちろんそれだけではありません。 「文字」を織り成す線には直線もあれば曲線もあり、また途中で曲げて角をつけることもあれば、その角度の付け方にもイメージが宿ります。
つまりは人が共通で持ち得る「あしらい」に対するイメージを積み重ねることよって、「書体」に「雰囲気」を持たせているのです。

例:ゴシック体

上記のことを前提として、「ゴシック体」の持っている雰囲気的なそれを例として書き出しておきたいと思います。

ゴシック体のイメージ :強い、重さ、頑健 f:id:astamuse:20170920115656p:plain なぜそういうイメージを持つか?:濃さは密度を連想させ、塗りの部分が多いため字が重く見える。また、角張っていてゴツゴツとしている所に男性的な印象を持つ。輪郭もはっきりとしているので、擬人化するとハキハキと大きな声でしゃべる男性が想像される。

(もう少し例を出しておきたいところですが、体力の限界なので終わります)

このように細かく分けて考えていくことで、「これめっちゃイケてる」と思った時に説明の手がかりになるはずです(多分)。

参考書籍:タイポグラフィの基本ルール-プロに学ぶ、一生枯れない永久不滅テクニック-[デザインラボ]

タイポグラフィの基本ルール -プロに学ぶ、一生枯れない永久不滅テクニック-[デザインラボ]

タイポグラフィの基本ルール -プロに学ぶ、一生枯れない永久不滅テクニック-[デザインラボ]

あと【書体編】と明記してありますが続きものというワケではないのでこれで終わるかもわからないなぁ…と 自分のプレッシャーにならない程度にゆるいペースでまとめて逝きたいです。

書体について教えてくれる人が居たら弊社にお越し下さいませ。

Copyright © astamuse company, ltd. all rights reserved.