rddの一般的な変換とアクション操作の詳細な説明

この記事は個人のパブリックアカウントから作成されました:TechFlow、オリジナルは簡単ではありません、注意を求めてください


今日はSpark3番目の記事です。RDDのいくつかの操作を引き続き調べます。

先ほど、sparkでのRDDの操作は、変換アクションの 2つのタイプに分けられると述べました変換操作では、sparkは結果を計算しませんが、新しいRDDノードを生成してこの操作を記録します。アクション操作が実行された場合にのみ、sparkは最初から計算全体を計算します。

変換操作は、要素の変換操作とコレクションの変換操作にさらに分けることができます。

要素固有の変換操作

要素の変換操作は非常に一般的であり、最も一般的に使用されるのはマップとフラットマップです。名前の観点からは、どちらもマップ操作ですが、マップ操作については誰もが知っているように、以前のMapReduce記事やPythonマップと使用量削減に関する記事で言及されています。つまり、操作を各要素にマッピングできます。

たとえば、シーケンス[1、3、4、7]があり、各要素を2乗したいとします。もちろんループの実行にも使用できますが、sparkのより良い方法はマップを使用することです。

nums = sc.parallelize([1, 3, 4, 7])
spuare = nums.map(lambda x: x * x)

mapは変換操作であることを知っているので、square はまだRDDであり、結果を取得せずに直接出力します。RDD関連情報のみです。

内部RDDの変換図は次のようになります。

結果を確認したい場合は、takeなどのアクション操作を実行する必要があります。結果を確認します。

私たちの期待と一致して、マップ操作は以前に注意を払っていた学生にはすでになじみがあるはずです。このフラットマップとは何ですか?

違いはこのフラットにありますフラットフラットを意味することは誰でも知っているので、flatmapはマップの実行結果がフラットであることを意味します。端的に言えば、つまり、マップの実行後の結果が配列の場合、その配列は逆アセンブルされ、中身が取り出されて結合されます。

例を見てみましょう:

texts = sc.parallelize(['now test', 'spark rdd'])
split = texts.map(lambda x: x.split(' '))

mapを実行するオブジェクトは文字列であるため、文字列の分割操作の後に文字列配列が取得されますmapを実行すると、結果は次のようになります。

フラットマップを実行するとどうなりますか?私たちはまた試すことができます:

比較して、違いに気づきましたか?

はい、マップの実行結果は配列の配列になります。これは、各文字列分割が配列になると、配列をつなぎ合わせるのは当然配列の配列になるためです。FlatMapは、これらの配列をフラット化してまとめます。これが2つの最大の違いです。

コレクションの変換アクション

要素の変換操作については上記で説明しています。コレクションの変換操作を見てみましょう。

コレクションの操作は、おそらく和集合、個別、交差、減算です。最初に次の図を見て直感的な感覚を得てから、それらを1つずつ分析します。

名前が示すように、重複を削除することです。SQLのdistinctと同じです。この操作の入力は2セットのRDDです。実行後、新しいRDDが生成されます。このRDDのすべての要素は一意です。注意すべきことの1つは、distinctを実行するコストが非常に大きいことです。これは、すべてのデータを無秩序化するシャッフル操作を実行して、各要素のコピーが1つだけであることを確認するためです。シャッフル操作の意味がわからなくても構いませんが、以降の記事で説明します。高価であることを覚えておいてください。

2番目の操作はunionであり、これも理解しやすく、2つのRDDのすべての要素マージすることですこれは、Pythonリストの拡張操作と考えることができます。これも拡張と同じです。重複する要素は検出されないため、2つのマージされたセットの同じ要素がフィルタリングされない場合、それらはフィルタリングされます。予約済み。

3番目の操作は交差、つまり交差、つまり2つのセットが重なる部分です。これはかなり理解できるはずです。次の図を見てみましょう。

2つのセットAとBの交差部分である下の図の青い部分は、2つのセットの共通要素であるA.intersection(B)の結果です。同様に、この操作はshuffleも実行するため、オーバーヘッドも同様に大きく、この操作は重複する要素を削除します。

最後の1つは減算です。これは差分セットです。これはAに属しているがBには属していない要素です。同様に、グラフを使用して以下を表すことができます。

上図の灰色の部分は、AとBの2つのセットの違いです。同様に、この操作でもシャッフルが実行されますが、非常に時間がかかります。

上記に加えて、デカルト、すなわちデカルト積、サンプルサンプリング、およびその他の集合演算がありますが、使用される量は比較的少なく、ここではあまり紹介されていません。興味のある学生はそれについて学ぶことができます。複雑。

アクション操作

RDDで最も一般的に使用されるアクション操作は、結果を取得する操作です。結局のところ、結果を取得するために半日計算したのですが、RDDを取得することは明らかに目的ではありません。結果を取得するためのRDDは、主にtake、top、およびcollectですが、これら3 つには特別な使用法はありません。

collectがすべての結果を取得する場合、すべての要素が返されます。takeとtopはどちらも、パラメーターを渡して項目指定する必要があります。Take は、指定された数の結果をRDDから返すことです。Topは、RDDから最初のいくつかの結果を返すことです。topとtakeの使い方はまったく同じですが、唯一の違いは得られた結果が最初かどうか。

これらに加えて、非常に一般的なアクションもカウントです。これは言うまでもありませんが、データ数を計算する操作では、カウントはデータの数を知ることができます。

減らす

これらの比較的単純なものに加えて、他に2つ紹介するのは興味深いことです。最初に、reduceを紹介しましょう。名前が示すように、ReduceはMapReduceのreduceです。その使用法はPythonのreduceほとんど同じです。マージ操作を実行する関数を受け入れます。例を見てみましょう:

この例では、reduce関数は2つのintを追加し、reduceメカニズムはこの操作を繰り返してすべてのデータをマージするため、最終結果は1 + 3 + 4 + 7 = 15になります。

折る

reduceに加えて、foldと呼ばれるアクションがあります。reduceとまったく同じです。唯一の違いは、初期値カスタマイズできることと、パーティション分割用であることです。上記の例も例として使用します。

この例を直接見るのは少々難しいかもしれませんが、簡単な説明で理解できますが、複雑ではありません。並列化を使用してデータを作成するときに、パーティションの数表すパラメーター2を追加したことに気付きましたSimpleは、配列[1、3、4、7]が2つの部分に分割されるので理解できますが、直接収集した場合でも、元の値のままです。

ここでフォールドを使用して、2つのパラメーターが渡され、関数に加えて初期値2が渡されます。したがって、計算プロセス全体は次のようになります。

最初のパーティションの答えは1 + 3 + 2 = 6、2番目のパーティションの答えは4 + 7 + 2 = 13、最後に2つのパーティションがマージされます:6 + 13 + 2 = 21。

つまり、各パーティションの結果に開始値割り当て、パーティションのマージ後の結果に開始値割り当てます。

集計

正直なところ、この行動は異常なので理解するのが最も難しい。まず、reduceとfoldのどちらにも、戻り値の型がrddのデータ型と同じでなければならないという要件があります。たとえば、データ型がintの場合、返される結果もintになります。

ただし、一部のシナリオではこれは適用できません。たとえば、平均化する場合は、用語の合計とその用語が出現する回数を知る必要があるため、2つの値を返す必要があります。この時点で、初期化した値は0、0、つまり合計とカウントの両方が0から始まる必要があります。次に、次のように2つの関数を渡す必要があります。

nums.aggregate((0, 0), lambda x, y: (x[0] + y, x[1] + 1), lambda x, y: (x[0] + y[0], x[1] + y[1]))

このコード行が馬鹿げているのを見ることは避けられません、心配しないでください、私たちはそれを少しずつ説明します。

1つ目は最初のラムダ関数です。xは値ではなく2つの値、または2つのタプルです。これは、最終的に返された結果です。戻り値の期待では、最初に返された数値はnumsの合計。2番目に返される数値は、nums内の数値の数です。ここのyはnums入力の結果ですが、明らかにnums入力の結果はintのみなので、ここのyは1次元です。次に、x [0] + yを必要とし、もちろん使用します。これは、yの値が最初の次元に追加され、2番目の次元が自然に1ずつ増加することを意味します。

この点は比較的理解しやすいです。2番目の関数は少し面倒かもしれません。2番目の関数は最初の関数とは異なります。これはnumsデータの処理には使用されませんが、パーティションの処理に使用されます集計を実行すると、sparkはシングルスレッドの実行ではなく、nums内のデータを多数のパーティションに分割します。各パーティションは結果を取得した後にマージする必要があり、この関数はマージ時に呼び出されます。

最初の関数と同様に、最初のxは最終結果であり、yは他のパーティション操作の最後にマージする必要がある値です。したがって、ここでのyは2次元であり、最初の次元は特定のパーティションの合計であり、2番目の次元は特定のパーティション内の要素の数です。もちろん、それをxに追加する必要があります。

上の図は、2つのパーティションの場合の計算プロセスを示しています。ここで、lambda1は最初に渡す無名関数です。同様に、lambda2は2番目に渡す無名関数です。組み合わせ図がわかりやすいと思います。

この他にもアクション操作がいくつかありますので、スペースの都合上繰り返しは行いませんが、次の記事でそれらがあった場合は詳しく説明します。初心者がスパークの学習に抵抗する主な理由の1つは、スパークが複雑すぎると感じ、操作でも変換操作とアクション操作を区別するためです。実際、これらはすべて、パフォーマンスを最適化するための遅延評価のためのものです。このように、複数の操作を組み合わせて実行することで、コンピューティングリソースの消費を削減できます。分散コンピューティングフレームワークの場合、パフォーマンスは非常に重要な指標です。これを理解すると、なぜsparkはそのような設計をしたのですか?わかりやすいです。

これは、sparkだけでなく、TensorFlowなどのディープラーニングフレームワークにも当てはまります。本質的に、直感に反するデザインの多くには、より深い理由があります。理解すると、実際に推測するのは簡単です。アクション演算、一部の計算のみの場合、10個中8個は変換演算です。

持久化操作

SparkのRDDは遅延評価されるため、同じRDDを複数回使用したい場合があります。アクション操作を呼び出すだけの場合、sparkはRDDとそれに対応するすべてのデータおよびその他の依存関係を繰り返し計算するため、明らかに多くのオーバーヘッドが発生します。私たちが頻繁に使用するRDDは、使用するたびに再度実行する必要がなく、必要なときにいつでもキャッシュして使用できることを期待しています。

この問題を解決するために、sparkは永続的な操作を提供しますいわゆるパーシスタンスは、キャッシングとして簡単に理解できます。使用方法も非常に簡単です。RDDを永続化するだけです。

texts = sc.parallelize(['now test', 'hello world'])
split = texts.split(lambda x: x.split(' '))
split.persist()

永続性を呼び出した後、RDDはメモリまたはディスクにキャッシュされ、プロセス全体を前に実行しなくても、必要なときにいつでも呼び出すことができます。また、sparkは複数レベルの永続化操作をサポートしており、StorageLevel変数を使用して制御できます。このStorageLevelの値を見てみましょう。

必要に応じて、対応するキャッシュレベルを選択できます。もちろん、永続性があるため、当然のことながら反永続性があります。キャッシュが不要になった一部のRDDでは、unpersistを呼び出してキャッシュから削除できます。

今日の内容はいろいろな操作のようですが、あまり使われていないものもあるので、感動さえあれば十分で、具体的な操作の詳細は、使うときにじっくり検討できます。誰もがこれらの重要でない詳細無視し、コアの本質を理解できることを願っています

今日の記事はそれだけです。もし何かやりがいを感じた場合は、フォローまたは再投稿しください。あなたの努力は私にとって非常に重要です。

ここに画像の説明を挿入

117件の元の記事を公開 61のような 10,000以上の訪問

おすすめ

転載: blog.csdn.net/TechFlow/article/details/105622005