データサイエンティストのツールボックス:解析

美とファッションのプロフェッショナルによる、データ解析のツールボックス

複雑なドキュメントを解析するのは、適切なツールがあれば簡単です

この記事で議論されている、新しいPythonベースのrd2mdパーサーとトランスフォーマーのソースコード。作者による画像

多くのデータサイエンティストにとって、複雑なドキュメントを利用できるデータに変換することは一般的な問題です。複雑なドキュメントを見て、データを変換するさまざまな方法を探ってみましょう。

TLDR;

次のようなルールを開発するにつれて探求します:

ルール1:必要なもの以上のことはやらないことルール2:問題の簡単な部分から始めることルール3:コードを捨ててやり直すことを恐れないことルール4:仕事をするために可能な限り簡単な方法を使用すること

問題

私はML企業の研究責任者として、探索や解決策の設計が必要なさまざまな問題に頻繁に直面します。先週、興味深い小さな問題が生じました:ML実験の重要な詳細を記録するためのオープンソースのR SDKであるComet R SDKのためにマークダウン形式のドキュメントを生成する方法が必要でした。そして、それを多くの時間を費やさずにすぐに解決策を見つける必要がありました。

この問題は、データサイエンティストが日常的に直面する問題よりも少し複雑かもしれませんが、さまざまなパース方法の使用例として役立ちます。そして、ボーナスとして、ペアリングの特定のニーズを満たす新しいオープンソースプロジェクトができます。さあ、始めましょう!

問題を聞いたら、私の研究開発(R&D)の第一のルールが働きました:

ルール1:必要なもの以上のことはやらないこと(怠惰さは、プログラマの3大美徳の1つとしてLarry Wallによって特定されました)。

そのため、Rコードをマークダウンに変換する方法が解決済みの問題か調べました。どうやら解決済みのようです!ただし、可能なすべてのプログラム(Rの古いRd2mdなど)を試した後、うまく動作せず、GitHubリポジトリもアクティブではありませんでした。では、私は一人でやることにしました。私がより優れたRプログラマーであれば、既存のソリューションを修正しようとしたかもしれません。しかし、私はPythonの方が好きなので、それをパースの例にします。そして、そうです、私たちはPythonでRドキュメンテーションをパースしています。

それでは、コードを書き始めました。そして、次のR&Dルールを思い出しました:

ルール2:問題の簡単な部分から始めること。

ルール2は、即座のフィードバックを得たいという私自身のニーズを満たす方法かもしれませんが、より重要な役割も果たします。簡単な部分から始めることで、難しい部分もそこまで難しくならないかもしれません。また、問題に取り組むためのウォーミングアップとしても役立ちます。通常、ソリューションのコーディングでは、1回または2回の見当外れな開始があります。これが次のルールにつながります:

ルール3:コードを捨ててやり直すことを恐れないこと!

最後に、正しい方向に進んでいるときの最後のルールは:

ルール4:仕事をするために可能な限り簡単な方法を使用すること。

さて、Rドキュメンテーションファイルをマークダウンに変換するための最も簡単な方法は何でしょうか?最初に、Rドキュメンテーションファイルとは何でしょうか?Rドキュメンテーションは、RコードからLaTeXに似たものに直接変換されます。以下はその例です(ファイルは.Rdで終わります):

% Generated by roxygen2: do not edit by hand% Please edit documentation in R/experiment.R\name{Experiment}\alias{Experiment}\title{A Comet Experiment object}\description{A comet experiment object can be used to modify or get information about an activeexperiment. All methods documented here are the different ways to interact with anexperiment. Use \code{\link[=create_experiment]{create_experiment()}} to create or \code{\link[=get_experiment]{get_experiment()}} toretrieve a Comet experiment object.}

目標は、LaTeXをmarkdownに変換し、以下のようなものにすることです:

## Description
アクティブな実験に関する情報を変更または取得するために、コメット実験オブジェクトが使用されることがあります。ここで文書化されているすべてのメソッドは、実験との対話方法の異なる方法です。コメット実験オブジェクトを作成するには、[`create_experiment()`](../create_experiment)を使用し、コメット実験オブジェクトを取得するには、[`get_experiment()`](../get_experiment)を使用します。

これは以下のように表示されます:

Example markdown output. Image by the author.

さて、非常にシンプルなものから始めましょう。以下のような形式でRdファイルを行ごとに処理します:

doc = Documentation()...for line in lines:    if line.startswith("%"):        pass    elif line.startswith("\\name{"):        matches = re.search("{(.*)}", line)        groups = groups()        name = groups[0]        doc.set_name(name)    ...

このコードでは、行が「%」で始まるかどうかを確認し、始まっている場合はスキップします(Rdファイル内のコメントにすぎません)。同様に、「\name」で始まる場合は現在のドキュメント名を設定します。「raw」Python文字列を使用しない場合はバックスラッシュをエスケープする必要があることに注意してください。コードre.search(“{(.*)}”, line)は、行に終わりの中カッコが含まれているという前提に基づいています。これは、SDKのすべての例で真実であるため、このコードを複雑にする必要はありません(ルール3に基づいて)。

ファイル内の行を処理する前に、Documentation()のインスタンスを作成することに注意してください。これを行うことで、すべての部分を収集し、最後にdoc.generate()を呼び出すことができます。マークダウンを逐次生成するのではなく、これを行う理由は、解析するアイテムのいくつかがマークダウン内で異なる順序で表示される場合があるためです。

次に、Rコードのいくつかを同様に処理できます。Rdファイルの行にパターンを探し、すぐに処理します。ただし、次に説明する方法では処理できないもう少し複雑な部分を見てみましょう:

\usage{create_experiment(  experiment_name = NULL,  project_name = NULL,  workspace_name = NULL,  api_key = NULL,  keep_active = TRUE,  log_output = TRUE,  log_error = FALSE,  log_code = TRUE,  log_system_details = TRUE,  log_git_info = FALSE)}

使用セクションは常に\usage{から始まり、単一の}で終わります。このような場合、これらの事実を使用して、やや複雑なパーサを作成できます:

...for line in lines:    ....    elif line.startswith("\\usage{"):        usage = ""        line = fp_in.readline().rstrip()        while line != "}":            usage += line + "\n"            line = fp_in.readline().rstrip()        doc.set_usage(usage)

これにより、行ごとにテキストを読み取り、\usage{}セクション内のすべてのテキストを収集します。

さらに複雑な部分に移ると、少し巧妙になり、初めて「状態」の考え方を使用する必要があります。

次のLaTeXコードを考えてみてください:

\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,warnings, and messages) will be redirected to the Comet servers to display as messagelogs for the experiment. Note that unlike \code{auto_log_output}, if this option is on thenthese messages will not be shown in the console and instead they will only be loggedto the Comet experiment. This option is set to \code{FALSE} by default because of thisbehavior.}

これはやや複雑です。トップレベルの形式は次のとおりです:

\item{NAME}{DESCRIPTION}

しかし、DESCRIPTION自体には中カッコで囲まれたアイテムが含まれる場合があります。このコードを文字列として扱えれば(改行も含めて)、Pythonのre(正規表現)モジュールを次のように使用できます:

text = """\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,warnings, and messages) will be redirected to the Comet servers to display as messagelogs for the experiment. Note that unlike \code{auto_log_output}, if this option is on thenthese messages will not be shown in the console and instead they will only be loggedto the Comet experiment. This option is set to \code{FALSE} by default because of thisbehavior.}"""matches = re.search("{(.*)}{(.*)}", text, re.DOTALL)

NAMEとDESCRIPTIONはmatches.groups()として取得することができます。reパターン“{(.*)}{(.*)}”のカッコは2つのグループをマッチさせるためのもので、最初のカッコの間の第1のグループと次のカッコの間の第2のグループをマッチさせます。これは、textがそのセクションだけであることを前提にうまく作動します。このセクションを最初に切り分けることなくパースできるようにするために、テキストを1文字ずつパースする必要があります。しかし、これは難しいことではありません。

以下の関数は、ファイルポインタ(またはモダンなPythonの言葉で言えば「ファイルのようなもの」)が与えられると、カーリーブレースの数を取得する簡単な関数です:

def get_curly_contents(number, fp):    retval = []    count = 0    current = ""    while True:        char = fp.read(1)        if char == "}":            count -= 1            if count == 0:                if current.startswith("{"):                    retval.append(current[1:])                elif current.startswith("}{"):                    retval.append(current[2:])                else:                    raise Exception("malformed?", current)                current = ""        elif char == "{":            count += 1        if len(retval) == number:            return retval        current += char

関数get_curly_contents()では、カーリーブレースのセクションの数とファイルポインタを渡します。したがって、ファイルから2つのカーリーブレースのセクションを取得するには、以下のようにします:

fp = open(FILENAME)name, description = get_curly_contents(2, fp)

get_curly_contents()は、このプロジェクトで取り組む中で最も複雑な部分の1つです。この関数は3つの状態変数retvalcountcurrentを持っています。 retvalはパースされたセクションのリストです。 countは現在のカーリーブレースアイテムの深さです。 currentは現在処理中の文字列です。 この関数は実際にはいくつかの場所で便利です。

最後に、さらに複雑なレベルがあります。 問題のエリアは、Rクラス定義のMethodのサブセクションです。以下は、簡略化された例です:

\if{html}{\out{<hr>}}\if{html}{\out{<a id="method-Experiment-new"></a>}}\if{latex}{\out{\hypertarget{method-Experiment-new}{}}}\subsection{Method \code{new()}}{Do not call this function directly. Use \code{create_experiment()} or \code{get_experiment()} instead.\subsection{Usage}{\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(  experiment_key,  project_name = NULL)}\if{html}{\out{</div>}}}\subsection{Arguments}{\if{html}{\out{<div class="arguments">}}\describe{\item{\code{experiment_key}}{The key of the \code{Experiment}.}\item{\code{project_name}}{The project name (can also be specified using the \code{COMET_PROJECT_NAME}}parameter as an environment variable or in a comet config file).}}\if{html}{\out{</div>}}}}

これは、ネストされたセクションを持つために複雑です。 UsageArgumentsMethodの内部にあります。この問題に対して完全なパースの武器を使って取り組みます。

少しでも自分自身への負担を軽減するために、最初にMethodサブセクションを「トークン化」します。それは、テキストを関連する文字列に分割するためのファンシーな言葉です。たとえば、次のLaTeXテキストを考えてみてください:

\subsection{Usage}{\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(  experiment_key,  project_name = NULL)}\if{html}{\out{</div>}}}

これは、次のような文字列のリストにトークン化される可能性があります:

[ "\\", "subsection", "{", "Usage", "}", "\\", "if", "{", "html", "}", "{", "\\", "out", "{", "<", "div",  " ",  "class", "=", "\"r\"", ">", "}", "}", "\\",  "preformatted", "{", "Experiment$new", "(", "experiment_key", "project_name", "=", "NULL", ")", "}", "\\", "if", "{", "html", "}", "{", "\\", "out", "{", "<", "/", "div", ">", "}", "}", "}"]

トークン化された文字列のリストは、それを簡単にサブパーツに分割して処理する能力を提供します。さらに、1つ以上のトークンを「先読み」して、どのようなものが来ているかを簡単に確認することができます。これは、正規表現やトークンではなく、個々の文字ではなくトークンを扱っている場合には難しいことです。以下は、トークン化されたセクションのパースの例です:

doc = Documentation()...method = Method()position = 0preamble = ""tokens = tokenize(text)while position < len(tokens):    token = tokens[position]    if token == "\\":        if tokens[position + 1] == "subsection":            in_preamble = False            if tokens[position + 3] == "Usage":                position, usage = get_tokenized_section(                    position + 5, tokens                )                 method.set_usage(usage)            elif tokens[position + 3] == "Arguments":                # skip this, we'll get with describe                position += 5            elif tokens[position + 3] == "Examples":                position, examples = get_tokenized_section(                    position + 5, tokens                )                method.set_examples(examples)            elif tokens[position + 3] == "Returns":                position, returns = get_tokenized_section(                    position + 5, tokens                )                 method.set_returns(returns)            else:                raise Exception("unkown subsection:", tokens[position + 3])        elif tokens[position + 1] == "describe":            position, describe = get_tokenized_section(position + 2, tokens)  # noqa            method.set_describe(describe)        else:            # \html            position += 1    else:        if in_preamble:            preamble += token        position += 1method.set_preamble(preamble)doc.add_method(method)

それだけです!完成したプロジェクトを見るには、新しいPythonベースのrd2mdをチェックアウトしてください。これは、RのRdファイルからマークダウンを生成するためのpipインストール可能なオープンソースのPythonライブラリです。私たちはこちらのRドキュメンテーションで使用しています:

https://www.comet.com/docs/v2/api-and-sdk/r-sdk/overview/

これは少しの午後のハックですか?はい。これは4つ以上の異なるパースメソッドから構成されています。しかし、これは仕事を遂行し、私が知っている中で唯一動作するRdからmarkdownへのコンバータです。もし再構築するならば、おそらく最後に示したメソッドを使用して、まずファイル全体をトークン化し、それから処理するでしょう。ルール3を覚えておいてください: コードを捨ててやり直すことに恐れを抱かないでください!

githubリポジトリへの貢献を希望される場合は、ぜひ参加してください。質問がある場合は、Issuesでお知らせください。

人工知能、機械学習、データサイエンスに興味がありますか?拍手とフォローをお考えください。Dougは、ML実験追跡とモデル監視の会社であるcomet.comのリサーチヘッドです。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more