やむやむもやむなし

やむやむもやむなし

自然言語処理やエンジニアリングのメモ

SQLで始める自然言語処理

こちらの記事はRecruit Engineers Advent Calendar 2020の24日目の記事です。メリークリスマス!

adventar.org


仕事の分析で使うデータはほとんどがBigQueryに保存されているため、基本的な分析作業の多くはBigQueryでSQLを書くことで行なっています。
BigQueryでテキストデータを扱おうと思うとSQLではできない or 取り回しが悪いことも多く、一度Pythonスクリプトを書いてその結果を再度BigQueryのテーブルに格納し、Joinして分析に使うということをしていました。

しかしこのやり方だとテキストデータを分析したいときは毎回Pythonのコードを書きにいかねばならず、またPythonでのテキスト処理も決して早いとはいえず、せっかくBigQueryでさくさく分析しているのにどうしてもテキスト処理に部分が作業時間のボトルネックになってしまいます。

SQLでテキスト処理を行うことができればいちいちPythonスクリプトを書くという面倒事に手を焼くこともなく、またPythonは書けないがSQLは書けるという人なら誰でもテキスト処理を行うことができるようになります。近年ではBigQueryやAmazon Redshiftなどの高度なデータウェアハウスが整備されていたり、SQLを使った分析技術に関する書籍が多数出版されていることもあり、SQLなら書けるという人も少なからずいるでしょう。

今回はこれらの要望を叶えるために、SQLを使って自然言語処理を行うテクニックを紹介します。

データ

今回も例によってLivedoorのブログコーパスを使用していきます。
データは「id」「ur」「write_date」「media」「title」「article」の6つのカラムに分けて格納しています。

f:id:ymym3412:20201222220614p:plain

前処理

まずはテキスト処理の基本、前処理を行なっていきます。
自然言語処理で行われているポピュラーな前処理をSQLで実現していきます。

正規表現による除去

基本中の基本の処理ですね。
BigQueryでは regexp_replace という関数が用意されており、それを使って正規表現にマッチしたテキストを置換することができます。

with texts as (
  select
    *
  from
    unnest(['昨日の<span color="red">夜</span>は非常に強い雨で<br>疲れてしまった',
            'なんとこれは<b>衝撃の真実</b>だったわけです']) as strings
)

select
  regexp_replace(strings, '<(".*?"|\'.*?\'|[^\'"])*?>', '') as clearned
from
  texts

f:id:ymym3412:20201222202023p:plain
結果

正規化

濁音のかなを表現する際、Unicodeには合成済み文字(だ)と基底文字+結合文字(た+゛)の2種類が存在します。
一見すると違いが分からない(実行する環境によってはほぼ同じに見える)ですが、文字列の一致判定をするとUnicodeが異なるため判定をすり抜けてしまい惨劇を招くことがあります。

f:id:ymym3412:20201222204938p:plain
見た目上は区別がつきづらい

これを回避するには文字列に対してUnicodeの正規化を行えばOKです。
BigQueryでは normalize という関数が用意されています。

with texts as (
  select
    'カピバラ' as a, 'カピバラ' as b
)

select
  a, b,
  a = b as raw,
  a = normalize(b, NFKC) as normalized
from
  texts

f:id:ymym3412:20201222205026p:plain
結果

単語分割

日本語の自然言語処理を行う場合にはテキストを形態素と呼ばれる単位に分割するのが一般的です。
これはMecabやSudachiといった形態素解析エンジンを使って行われることがほとんどです。

しかしSQLでこれらのミドルウェアPythonコードを動かすことができません。
そこで使うのがBigQueryの User Defined Function(UDF)機能です。

User Defined Function機能とはBigQueryが提供している機能のひとつで、これを使えば外部からSQLまたはJavaScriptを読み込んで関数として実行することができます。
つまりJavaScriptに処理を書き込めばSQLの中で何でも実行することができるわけです!

JavaScriptで単語分割を行うライブラリとしてTinySegmenterがあります。
ただしこれはニュース記事をもとに学習しているため、他のドメインのテキストの単語分割では精度が出しづらい可能性があります。

そんなときのために、TinySegmenterを再学習させることができるTinySegmenterMakerというツールがあるので、今回はこれを使ってブログドメイン向けの形態素解析器を作成しました。

長いので一部のみ記載

// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript
// (c) 2008 Taku Kudo <taku@chasen.org>
// TinySegmenter is freely distributable under the terms of a new BSD licence.
// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt

(function(global) {
    global.TinySegmenter = TinySegmenter;
    var default_model = {BC1:...
        return result;
    };
})(this);

const tinySegmenter = new TinySegmenter();

function segment(x){
    return tinySegmenter.segment(x);
  }

このJavaScriptGoogle Cloud Storageに配置し、そのファイルをUDFの記法で読み込めばSQLの中で形態素解析を行うことができるようになります。

create temporary function segment(x string)
returns array<string>
language js as """
  return segment(x)
"""
options (
  library="gs://ymym-example-project/udf/string/tinysegmenter_retrain.js"
);

with segmented as (
  select
    *
  from
    unnest(['昨日の夜は非常に強い雨で疲れてしまった',
                'なんとこれは衝撃の事実だったわけです']) as strings
)

select
  segment(strings) as tokens
from
  segmented

f:id:ymym3412:20201222212447p:plain
形態素解析結果がrepetedなカラムで得られる

ストップワード除去

テキストを扱う上であまり大きな意味を持たないことが多い語をストップワードと呼びます。
日本語の場合、「は」や「の」、「あっち」のような助詞や指示語などが該当することが多いです。

ストップワードについては公開されている日本語ストップワード辞書があるのでそれをテーブルにして単語分割結果の各tokenがその辞書の中に存在するかでチェックをかければOKです。

f:id:ymym3412:20201222230031p:plain
ストップワードのテーブル

create temporary function segment(x string)
returns array<string>
language js as """
  return segment(x)
"""
options (
  library="gs://ymym-example-project/udf/string/tinysegmenter_retrain.js"
);

# 形態素解析結果
with segmented as (
  select
   id, segment(article) as seg
  from
    `ymym-example-project.work.livedoor_corpus` 
),

token_base as (
  select
    id, token
  from
    segmented, unnest(seg) as token
)

select
  *
from
  token_base
where
  token not in (select  words  from `ymym-example-project.work.stop_words_ja` ) # ストップワードのテーブル

頻度による除去

ストップワードの他にも特定のドメインでは当たり前のように出てくる単語は分析の際に邪魔になってしまうこともあります。
なので分析の際に頻度が高すぎる/低すぎる語を除外することがあります。
これは頻度順TopNの除外と、頻度がM以下の語を除外といった形で実現されます。

# 最低頻度
create temporary function min_freq() AS (3);
# 除外するTopN
create temporary function top_n() AS (200);

token_base as (
  select
    id, token
  from
    segmented, unnest(seg) as token
),

# 最低頻度に達していないtokenをフィルタリングするテーブル
token_filter as(
  select
    token
  from
    token_base
  group by token
  having
    count(1) <= min_freq()
),

# 頻度上位N個の除外する単語
top_n_filter as (
  select
    token
  from
    (
      select
        token,
        row_number() over (order by count(1) desc) as seq
      from
        token_base
      group by token
    )
  where
    seq <= top_n()
),

# tokenのフィルタリング
filtered_token as(
  select
    kuchikomi_cd, token
  from
    token_base
  where
    token not in (select token from token_filter)
  and
    token not in (select token from top_n_filter)
  and
    token not in (select  word  from `ymym-example-project.work.stop_words_ja` )

ストップワードのフィルターと合わせることで分析に不必要な単語を除外することができました。

TF-IDF

TF-IDFは文書中の単語の重要度を計算する手法です。
細かい説明は省きますが、文書での単語の出現比率を表すTerm Frequency、全文書での単語の出現する文書の比率の逆数を表すInverse Document Frequencyをかけあわせることで計算できます。
通常のTF-IDFでは文書の長さでスコアが変化しやすいため、文書の長さで正規化した値を用いることが多いです。

TF-IDFの計算は単純に頻度を数え上げればできるので、ここまででフィルタリングした結果を元にTF-IDFを計算していきます。

Term Frequency

まずはTF値を計算します。

# tf計算
tf_base as(
  select
    id, token, count(1) as cnt,
  from
    filtered_token
  group byid, token
),

term_in_doc as (
  select
   id, count(1) as len
  from
    filtered_token
  group byid
),

tf_table as (
  select
   id, token, (cnt / len) as tf
  from
    tf_base
  inner join term_in_doc
    using (id)
)

select
  *
from
  tf_table

f:id:ymym3412:20201222233003p:plain
文書中の単語に対するTF値

Inverse Document Frequency

続いてIDF値の計算です。

# idf計算
df_base as (
  select distinct
    id, token
  from
    filtered_token
),

doc_len as (
  select
    count(1) as doc_count 
  from
    (
      select distinct
        id
      from
        filtered_token
    )
),

df_agg as (
  select
    token, count(*) as cnt
  from
    df_base
  group by token
),
df_table as (
  select
  token, log(doc_count / cnt) + 1 as idf
  from
    df_agg
  cross join doc_len
)

select
  *
from
  df_table

f:id:ymym3412:20201222234049p:plain
IDFの計算結果

L2正規化済みTF-IDF

ではここまでで求めたTF値とIDF値を組み合わせてTF-IDF値を計算しつつ、文書の長さの影響を緩和するためにL2正規化を行います。

tfidf_table as (
  select
    id, tf_table.token, tf * idf as tfidf
  from
    tf_table
  cross join
    df_table
  where
    tf_table.token = df_table.token
),

# L2正規化
l2_normalized_tfidf as(
  select
    id, token, tfidf / sqrt(sum(tfidf * tfidf) over (partition by id)) as l2_tfidf
  from
    tfidf_table
)

select
  *
from
  l2_normalized_tfidf

f:id:ymym3412:20201222235441p:plain
文書の単語毎のTF-IDF値

TF-IDFとCos類似度を使った類似文書検索

ここまでで計算したTF-IDF値を使って文書間の類似度をCos類似度で計算します。
先ほど作成した l2_normalized_tfidf テーブルはその文書に出現する単語のみを保持しているので2文書間のCos類似度を測る時は双方の文書に出現する単語のTF-IDF値の積を取って全て足し合わせればOKです。
またL2正規化をしてあるため、Cos類似度の分母の計算は必要ありません。

# TF-IDFベクトルのCosine類似度を計算
# 片方の文書にしか出現していないtokenは、もう片方のtfidf値が0なので考慮する必要がない
# 双方に出現するtokenのtfidf値の積だけ考えればよい
cos_similarity as (
  select distinct
    a.id as a_id, b.id as b_id, sum(a.l2_tfidf * b.l2_tfidf) over (partition by a.id, b.id) as cos_sim,
  from
    l2_normalized_tfidf as a
  cross join
    l2_normalized_tfidf as b
  where
    a.token = b.token
  and
    a.id <> b.id
)

select
  a_id, b_id, cos_sim, a_article.media, b_article.media, a_article.article, b_article.article
from
  (
    select
      a_id, b_id, cos_sim,
      row_number() over (partition by a_id order by cos_sim desc) as seq
    from
      cos_similarity
  )
# 文書の中身を確認するためjoinする
inner join `ymym-example-project.work.livedoor_corpus` as a_article on a_id = a_article.id
inner join `ymym-example-project.work.livedoor_corpus` as b_article on b_id = b_article.id
# 類似度が高い上位5件を取得
where
  seq <= 5
order by a_id, seq

f:id:ymym3412:20201223034857p:plain
Cos類似度による類似文書

結果を見てみると、「dokujo-tsushin」の文書aに対して、上位の類似文書も「dokujo-tsushin」であり、前処理の効果もありうまく特徴量抽出ができているようです。

また処理時間についても、7400件弱のブログの本文のテキストに対し、前処理〜単語分割〜TF-IDF計算〜Cos類似度計算の全てを行っても300秒強程度しかかからず、かなりの速度で一連の作業を終えられているのではないかと思います。

最後にSQLの全体像をまとめておきます。

# tinysegmenterによる単語分割
create temporary function segment(x string)
returns array<string>
language js as """
  return segment(x)
"""
options (
 library="gs://ymym-exapmle-project/udf/string/tinysegmenter_retrain.js"
);

create temporary function min_freq() AS (3);
create temporary function top_n() AS (200);

# 形態素解析結果
with segmented as (
  select
   id, segment(article) as seg
  from
    `ymym-example-project.work.livedoor_corpus` 
),

token_base as (
  select
    id, token
  from
    segmented, unnest(seg) as token
),

# 最低頻度に達していないtokenをフィルタリングするテーブル
token_filter as(
  select
    token
  from
    token_base
  group by token
  having
    count(1) <= min_freq()
),

# 頻度上位N個の除外する単語
top_n_filter as (
  select
    token
  from
    (
      select
        token,
        row_number() over (order by count(1) desc) as seq
      from
        token_base
      group by token
    )
  where
    seq <= top_n()
),

# ストップワードなどを含めたtokenのフィルタリング
filtered_token as(
  select
    id, token
  from
    token_base
  where
    token not in (select token from token_filter)
  and
    token not in (select token from top_n_filter)
  and
    token not in (select  words  from `ymym-example-project.work.stop_words_ja` ) # ストップワードのテーブル
),
# tf計算
tf_base as(
  select
    id, token, count(1) as cnt,
  from
    filtered_token
  group by id, token
),

term_in_doc as (
  select
    id, count(1) as len
  from
    filtered_token
  group by id
),

tf_table as (
  select
    id, token, (cnt / len) as tf
  from
    tf_base
  inner join term_in_doc
    using (id)
),

# idf計算
df_base as (
  select distinct
    id, token
  from
    filtered_token
),

doc_len as (
  select
    count(1) as doc_count 
  from
    (
      select distinct
        id
      from
        filtered_token
    )
),

df_agg as (
  select
    token, count(*) as cnt
  from
    df_base
  group by token
),
df_table as (
  select
  token, log(doc_count / cnt) + 1 as idf
  from
    df_agg
  cross join doc_len
),

tfidf_table as (
  select
    id, tf_table.token, tf * idf as tfidf
  from
    tf_table
  cross join
    df_table
  where
    tf_table.token = df_table.token
),

# L2正規化
l2_normalized_tfidf as(
  select
    id, token, tfidf / sqrt(sum(tfidf * tfidf) over (partition by id)) as l2_tfidf
  from
    tfidf_table
),

# TF-IDFベクトルのCosine類似度を計算
# 片方の文書にしか出現していないtokenは、もう片方のtfidf値が0なので考慮する必要がない
# 双方に出現するtokenのtfidf値の積だけ考えればよい
cos_similarity as (
  select distinct
    a.id as a_id, b.id as b_id, sum(a.l2_tfidf * b.l2_tfidf) over (partition by a.id, b.id) as cos_sim,
  from
    l2_normalized_tfidf as a
  cross join
    l2_normalized_tfidf as b
  where
    a.token = b.token
  and
    a.id <> b.id
)

select
  a_id, b_id, cos_sim, a_article.media, b_article.media, a_article.article, b_article.article
from
  (
    select
      a_id, b_id, cos_sim,
      row_number() over (partition by a_id order by cos_sim desc) as seq
    from
      cos_similarity
  ) tmp
inner join `ymym-example-project.work.livedoor_corpus` as a_article on a_id = a_article.id
inner join `ymym-example-project.work.livedoor_corpus` as b_article on b_id = b_article.id
where
  seq <= 5
order by a_id, seq

Embeddingを使った類似文検索

TF-IDFみたいな疎ベクトルって古くない?という方のためにも、近年主流である文章を密なベクトル(Embedding)に変換して類似文書を検索していくやり方も紹介していきます。

先ほどまでのTF-IDFでは、2つの文書間の単語で、単語の表層が一致していないと類似度の計算ができませんでしたが、例えば密ベクトルの中に「ゆったり」と「のんびり」が意味的に近いということをエンコードできれば、単語の表層が一致していなくても意味的に類似している文書を探しやすくなることが期待できます。

しかし、文書のEmbeddingを得るのはTF-IDFのように単語分割した結果を集計するだけではできません。
Embeddingを得るための何らかのモデルに単語分割した単語列を入力する必要があります。

今回はBigQuery MLを使って文書のEmbeddingを取得していきます。

BigQuery ML

BigQuery MLはBigQueryが提供している機能のひとつで、SQLを使ってモデルを学習させたり、学習させたモデルを使った推論などを行うことができます。
学習できるモデルは線形回帰、ロジスティック回帰、k-means、行列分解などメジャーな手法がカバーされています。

また自分でモデルを学習させる他にも、TensorFlowモデルのインポートにも対応しており、今回はそちらを使ってEmbeddingを得るためのモデルを外部からBigQueryに持ち込みます。

TensorFlow HubからNNLMを持ち込む

TensorFlow Hub(TF Hub)というTFの学習済みモデルをホスティングするサービスがあり、今回はそこから日本語のNeural Network Language Model(NNLM)を使って日本語文書のEmbeddingを計算します。
BigQuery MLでTFのモデルを読み込むにはTFのモデルをSavedModel形式で保存している必要があるので、TF Hubからモデルをダウンロードし、そのモデルをSavedModel形式で保存してGCSに持ち込みます。

import tensorflow_hub as hub
import tensorflow as tf


class NNLM(tf.Module):
    def __init__(self, model):
        self.model = model

    @tf.function(input_signature=[tf.TensorSpec(shape=None, dtype=tf.string)])
    def __call__(self, x):
        result = self.model(x)
        return {"scores": result }


# TF Hubからモデルのダウンロード
embed = hub.KerasLayer("https://tfhub.dev/google/nnlm-ja-dim50-with-normalization/2",
                           input_shape=[], dtype=tf.string)

# SavedModel形式で保存できるようにクラスでラップする
model = tf.keras.Sequential([embed])
module = NNLM(model)

call_output = module.__call__.get_concrete_function(tf.TensorSpec(None, tf.string))
# nnlm/以下に保存されたファイルをGCSに保存する
tf.saved_model.save(module, "./nnlm", signatures={'serving_default': call_output})

これでHubからモデルをダウンロードしてBigQuery MLで読み込めるようになりました。

BigQuery MLで文書をEmbedding化

今回使用するNNLMは文書を単語分割しそれらを半角スペースで繋げたものを入力として想定しているので、文書を変換してからモデルに入力します。
BigQuery MLでのモデルのロードには専用の記法があり、それにモデルを保存してGCSのバケットディレクトリを指定すればモデルをロードできます。

# BQ MLでtensorflow hubにあるnnlmモデルを保存したものを読み込む
create or replace model `ymym-example-project.work.imported_tf_model`
   options (model_type='tensorflow',
    model_path='gs://ymym-example-project/bqml/use-multi/nnlm/nnlm/*');

# tinysegmenterによる単語分割
create temporary function segment(x string)
returns array<string>
language js as """
  return segment(x)
"""
options (
 library="gs://ymym-exapmle-project/udf/string/tinysegmenter_retrain.js"
);

# 形態素解析結果
with segmented as (
  select
   id, segment(article) as seg
  from
    `ymym-example-project.work.livedoor_corpus` 
),

# BQ MLでテキストをembeddingに変換
# テキストはtokenを半角スペースでつないだものを入力とする
nnlm_predict as (
  select
    *
  from ml.predict(model `ymym-example-project.work.imported_tf_model`,
     (
      select
        # TensorFlowのモデルで受け取る変数名: xに命名しなおす
        id, array_to_string(seg, ' ') as x
      from
        segmented
     )
  )
)

select
  *
from
  nnlm_predict

うまくいけば、このようにベクトルの要素がrepeatedなカラムに格納された形で取得できます。

f:id:ymym3412:20201223015325p:plain
文書のEmbedding

Cos類似度を用いた類似文書検索

文書毎の埋め込みベクトルを得ることができたので、あとはTF-IDFのときと同じようにCos類似度を求めていくだけです。

# 文書A, 文書B, Aのベクトル, Bのベクトルというテーブルを作る
doc_vector as(
  select
    A.id as doc_a, B.id as doc_b, A.scores as score_a, B.scores as score_b, 
  from
    nnlm_predict as A, nnlm_predict as B
),

# コサイン類似度を計算
doc_similarity as(
  select
    doc_a, doc_b,
    (
      select
        sum(k * u) / (sqrt(sum(k * k)) * sqrt(sum(u * u)))
      from
        unnest(score_a) as k with offset as i, unnest(score_b) as u with offset as j
      where
        i = j
    ) as cos_sim
  from
    doc_vector
  order by doc_a, doc_b
)

select
  doc_a, doc_b, cos_sim, rank, article_a.media as media_a, article_b.media as media_b, article_a.article as article_a, article_b.article as article_b
from
  (
    select
      doc_a, doc_b, cos_sim,
      row_number() over (partition by doc_a order by cos_sim desc) as rank
    from
      doc_similarity
    where
      doc_a <> doc_b
  )
inner join
  `ymym-example-project.work.livedoor_corpus` as article_a
on doc_a = article_a.id
inner join
  `ymym-example-project.work.livedoor_corpus`  as article_b
on doc_b = article_b.id
where
  rank <= 5
order by doc_a, rank

類似文書検索結果を見てみます。

f:id:ymym3412:20201223021522p:plain
文書のEmbeddingによる類似文書検索

こちらでも先ほどと同じように「dokujo-tsushin」の文書に対して「dokujo-tsushin」の文書が類似度上位に来ています。
またTF-IDFのときとは違って、中に出てくるトピックは違うが女性向けの記事であるという点で近い内容であることが分かります。

こちらも最後にSQLの全体像を載せておきます。

# BQ MLでtensorflow hubにあるnnlmモデルを保存したものを読み込む
create or replace model `ymym-example-project.work.imported_tf_model`
   options (model_type='tensorflow',
    model_path='gs://ymym-example-project/bqml/use-multi/nnlm/nnlm/*');

# tinysegmenterによる単語分割
create temporary function segment(x string)
returns array<string>
language js as """
  return segment(x)
"""
options (
 library="gs://ymym-exapmle-project/udf/string/tinysegmenter_retrain.js"
);

# 形態素解析結果
with segmented as (
  select
   id, segment(article) as seg
  from
    `ymym-example-project.work.livedoor_corpus` 
),

# BQ MLでテキストをembeddingに変換
# テキストはtokenを半角スペースでつないだものを入力とする
nnlm_predict as (
  select
    *
  from ml.predict(model `ymym-example-project.work.imported_tf_model`,
     (
      select
        # TensorFlowのモデルで受け取る変数名: xに命名しなおす
        id, array_to_string(seg, ' ') as x
      from
        segmented
     )
  )
),

# 文書A, 文書B, Aのベクトル, Bのベクトルというテーブルを作る
doc_vector as(
  select
    A.id as doc_a, B.id as doc_b, A.scores as score_a, B.scores as score_b, 
  from
    nnlm_predict as A, nnlm_predict as B
),

# コサイン類似度を計算
doc_similarity as(
  select
    doc_a, doc_b,
    (
      select
        sum(k * u) / (sqrt(sum(k * k)) * sqrt(sum(u * u)))
      from
        unnest(score_a) as k with offset as i, unnest(score_b) as u with offset as j
      where
        i = j
    ) as cos_sim
  from
    doc_vector
  order by doc_a, doc_b
)

select
  doc_a, doc_b, cos_sim, rank, article_a.media as media_a, article_b.media as media_b, article_a.article as article_a, article_b.article as article_b
from
  (
    select
      doc_a, doc_b, cos_sim,
      row_number() over (partition by doc_a order by cos_sim desc) as rank
    from
      doc_similarity
    where
      doc_a <> doc_b
  )
inner join
  `ymym-example-project.work.livedoor_corpus` as article_a
on doc_a = article_a.id
inner join
  `ymym-example-project.work.livedoor_corpus`  as article_b
on doc_b = article_b.id
where
  rank <= 5
order by doc_a, rank

BigQuery MLの注意点

BigQuery MLを使う上で注意しなくてはならないのがインポートできるモデルのサイズ上限です。
2020/12現在ののBigQuery MLでのTF Modelインポート機能では、モデルのサイズは250MBに制限されています。
このサイズは意外とシビアで、今回使ったNNLMは文書を50次元のベクトルに埋め込むモデルだったのですが、これを128に埋め込むモデルにするとサイズオーバーでインポートすることができません。
そのため近年流行りのBERTのような巨大モデルはもちろん、Universal Sentence Encoder(293MB...)のようなTF Hubで提供されている有用なモデルも今の所読み込むことができません。
Googleさん、モデルサイズの上限をもう少しだけ緩和して頂けないでしょうか...

まとめ

今回はSQLのみを使って自然言語処理の前処理、そしてTF-IDFと文書のEmbeddingを使った類似文検索を行うやり方を紹介しました。
これらをSQLだけで完結させることができれば、テキスト処理のたびにわざわざPythonを書かなくてもよくなるので、分析のスピードが上がるのではないでしょうか。
またBigQueryは内部で処理を分散して非常に高速に行ってくれるため、通常は非常に時間がかかるテキスト処理もBigQueryでやれば短い時間で済ますことができます。

SQL NLPerが今後増えていくことを楽しみにしています。

参考文献