Как сохранить пользовательские объекты в наборе данных?
В соответствии с введением наборов данных Spark:
В преддверии Spark 2.0 мы планируем несколько интересных улучшений в наборах данных, в частности:... Пользовательские кодировщики - в то время как в настоящее время мы автоматически генерируем кодировщики для широкого спектра типов, нам хотелось бы открыть API для пользовательских объектов.
и попытки сохранить пользовательский тип в Dataset
приводят к следующей ошибке, такой как:
Невозможно найти кодировщик для типа, хранящегося в наборе данных. Примитивные типы (Int, String и т.д.) И типы Product (классы дел) поддерживаются путем импорта sqlContext.implicits._ Поддержка сериализации других типов будет добавлена в будущих выпусках.
или же:
Java.lang.UnsupportedOperationException: не найден кодировщик для....
Существуют ли обходные пути?
Обратите внимание, что этот вопрос существует только в качестве отправной точки для ответа сообщества Wiki. Не стесняйтесь обновлять/улучшать как вопрос, так и ответ.
Ответы
Ответ 1
Update
Этот ответ по-прежнему остается актуальным и информативным, хотя теперь все лучше и лучше с 2.2/2.3, что добавляет поддержку встроенного энкодера для Set
, Seq
, Map
, Date
, Timestamp
и BigDecimal
. Если вы придерживаетесь создания типов только для классов case и обычных типов Scala, вы должны быть в порядке с неявным в SQLImplicits
.
К сожалению, практически ничего не добавлено, чтобы помочь в этом. Поиск @since 2.0.0
в Encoders.scala
или SQLImplicits.scala
находит вещи в основном для примитивных типов (и некоторой настройки классов case). Итак, первое, что можно сказать: , в настоящее время нет реальной хорошей поддержки кодировщиков пользовательского класса. Из-за этого следует, что некоторые из трюков, которые делают так же хорошо, как мы можем когда-либо надеяться, учитывая то, что у нас есть в настоящее время. Как предварительный отказ от ответственности: это не сработает отлично, и я сделаю все возможное, чтобы сделать все ограничения понятными и заранее.
В чем именно проблема
Если вы хотите создать набор данных, Spark "требует кодировщика (для преобразования объекта JVM типа T в и из внутреннего представления Spark SQL), который обычно создается автоматически через implicits от SparkSession
, или может быть созданный явно путем вызова статических методов на Encoders
" (взято из docs на createDataset
). Кодер примет форму Encoder[T]
, где T
- тип, который вы кодируете. Первое предложение состоит в том, чтобы добавить import spark.implicits._
(который дает вам эти неявные кодеры), а второе предложение - явно передать неявный кодер, используя этот набор связанных с кодировщиками функций.
Для обычных классов отсутствует кодировщик, поэтому
import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
предоставит вам следующую неявную связанную ошибку времени компиляции:
Невозможно найти кодировщик для типа, хранящегося в наборе данных. Примитивные типы (Int, String и т.д.) И типы продуктов (классы case) поддерживаются при импорте sqlContext.implicits._ Поддержка сериализации других типов будет добавлена в будущих выпусках
Однако, если вы обертываете любой тип, который вы использовали для получения вышеуказанной ошибки в каком-то классе, который расширяет Product
, ошибка с запутанностью задерживается во время выполнения, поэтому
import spark.implicits._
case class Wrap[T](unwrap: T)
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(Wrap(new MyObj(1)),Wrap(new MyObj(2)),Wrap(new MyObj(3))))
Скомпилируется просто отлично, но не работает во время выполнения с
java.lang.UnsupportedOperationException: No Encoder не найден для MyObj
Причина этого в том, что кодеры, создаваемые Spark с implicits, фактически выполняются только во время выполнения (через Scala relfection). В этом случае все проверки Spark во время компиляции заключаются в том, что внешний класс расширяет Product
(что все классы классов) и реализует только во время выполнения, что он все еще не знает, что делать с MyObj
(та же проблема возникает, если я попытался сделать Dataset[(Int,MyObj)]
- Spark ждет, пока время выполнения не будет установлено на MyObj
). Это центральные проблемы, которые остро нуждаются в исправлении:
- некоторые классы, которые расширяют
Product
, компилируются, несмотря на всегда сбой во время выполнения и
- В пользовательских кодировщиках нет способов передачи вложенных типов (у меня нет возможности кормить Spark encoder только для
MyObj
, чтобы он знал, как кодировать Wrap[MyObj]
или (Int,MyObj)
).
Просто используйте kryo
Решение, предлагаемое всеми, заключается в использовании kryo
encoder.
import spark.implicits._
class MyObj(val i: Int)
implicit val myObjEncoder = org.apache.spark.sql.Encoders.kryo[MyObj]
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
Это довольно утомительно. Особенно, если ваш код манипулирует множеством наборов данных, объединяется, группируется и т.д. В итоге вы получаете множество дополнительных имплицитов. Итак, почему бы просто не сделать неявное, что делает это все автоматически?
import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) =
org.apache.spark.sql.Encoders.kryo[A](ct)
И теперь кажется, что я могу делать почти все, что захочу (пример ниже не будет работать в spark-shell
, где spark.implicits._
автоматически импортируется)
class MyObj(val i: Int)
val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).alias("d2") // mapping works fine and ..
val d3 = d1.map(d => (d.i, d)).alias("d3") // .. deals with the new type
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1") // Boom!
Или почти. Проблема в том, что использование kryo
приводит к тому, что Spark просто сохраняет каждую строку в наборе данных как плоский двоичный объект. Для Map
, filter
, foreach
этого достаточно, но для операций, подобных join
, Spark действительно нуждается в том, чтобы их разделили на столбцы. Проверяя схему для d2
или d3
, вы видите, что есть только один двоичный столбец:
d2.printSchema
// root
// |-- value: binary (nullable = true)
Частичное решение для кортежей
Итак, используя магию implicits в Scala (подробнее в 6.26.3 Перегрузка Разрешение), я могу сделать себе серию подразумевает, что будет делать как можно более хорошую работу, по крайней мере для кортежей, и будет хорошо работать с существующими имплицитами:
import org.apache.spark.sql.{Encoder,Encoders}
import scala.reflect.ClassTag
import spark.implicits._ // we can still take advantage of all the old implicits
implicit def single[A](implicit c: ClassTag[A]): Encoder[A] = Encoders.kryo[A](c)
implicit def tuple2[A1, A2](
implicit e1: Encoder[A1],
e2: Encoder[A2]
): Encoder[(A1,A2)] = Encoders.tuple[A1,A2](e1, e2)
implicit def tuple3[A1, A2, A3](
implicit e1: Encoder[A1],
e2: Encoder[A2],
e3: Encoder[A3]
): Encoder[(A1,A2,A3)] = Encoders.tuple[A1,A2,A3](e1, e2, e3)
// ... you can keep making these
Затем, вооружившись этими имплицитами, я могу сделать свой пример выше работы, хотя и с некоторым переименованием столбцов
class MyObj(val i: Int)
val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d2")
val d3 = d1.map(d => (d.i ,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d3")
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1")
Я еще не понял, как получить ожидаемые имена кортежей (_1
, _2
,...) по умолчанию без их переименования - если кто-то хочет поиграть с этим, это, где вводится имя "value"
и это, где кортеж имена обычно добавляются. Однако ключевым моментом является то, что у меня теперь есть хорошая структурированная схема:
d4.printSchema
// root
// |-- _1: struct (nullable = false)
// | |-- _1: integer (nullable = true)
// | |-- _2: binary (nullable = true)
// |-- _2: struct (nullable = false)
// | |-- _1: integer (nullable = true)
// | |-- _2: binary (nullable = true)
Итак, вкратце, это обходное решение:
- позволяет нам получить отдельные столбцы для кортежей (так что мы снова можем присоединиться к кортежам, yay!)
- мы снова можем просто полагаться на имплициты (поэтому нет необходимости проходить в
kryo
по всему месту)
- почти полностью обратно совместим с
import spark.implicits._
(с некоторым переименованием)
- не позволяет нам присоединяться к сериализованным двоичным столбцам
kyro
, не говоря уже о полях, которые могут иметь
- имеет неприятный побочный эффект переименования некоторых столбцов кортежа в значение "значение" (при необходимости это можно отменить, преобразование
.toDF
, определение новых имен столбцов и преобразование обратно в набор данных - и имена схем похоже, сохраняются через соединения, где они наиболее необходимы).
Частичное решение для классов вообще
Этот менее приятный и не имеет хорошего решения. Однако теперь, когда у нас есть решение кортежа выше, у меня есть догадка, что неявное решение для преобразования из другого ответа будет немного менее болезненным, так как вы можете преобразовать более сложные классы в кортежи. Затем, создав набор данных, вы, вероятно, переименуете столбцы, используя подход dataframe. Если все идет хорошо, это действительно улучшается, так как теперь я могу выполнять объединения в полях моих классов. Если бы я просто использовал один плоский двоичный сериализатор kryo
, который не был бы возможен.
Вот пример, который выполняет немного всего: у меня есть класс MyObj
, который имеет поля типов Int
, java.util.UUID
и Set[String]
. Первый заботится о себе. Второй, хотя я мог бы сериализовать использование kryo
, был бы более полезен, если бы он был сохранен как String
(так как UUID
, как правило, я хочу присоединиться к нему). Третий действительно просто принадлежит двоичному столбцу.
class MyObj(val i: Int, val u: java.util.UUID, val s: Set[String])
// alias for the type to convert to and from
type MyObjEncoded = (Int, String, Set[String])
// implicit conversions
implicit def toEncoded(o: MyObj): MyObjEncoded = (o.i, o.u.toString, o.s)
implicit def fromEncoded(e: MyObjEncoded): MyObj =
new MyObj(e._1, java.util.UUID.fromString(e._2), e._3)
Теперь я могу создать набор данных с красивой схемой, используя эту технику:
val d = spark.createDataset(Seq[MyObjEncoded](
new MyObj(1, java.util.UUID.randomUUID, Set("foo")),
new MyObj(2, java.util.UUID.randomUUID, Set("bar"))
)).toDF("i","u","s").as[MyObjEncoded]
И схема показывает мне столбцы с правильными именами и двумя первыми, с чем я могу присоединиться.
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
Ответ 2
-
Использование универсальных кодеров.
На данный момент доступны два общих кодера kryo
и javaSerialization
, где последний явно описывается как:
крайне неэффективен и должен использоваться только в качестве последнего средства.
Предполагая следующий класс
class Bar(i: Int) {
override def toString = s"bar $i"
def bar = i
}
вы можете использовать эти кодеры, добавив неявный кодировщик:
object BarEncoders {
implicit def barEncoder: org.apache.spark.sql.Encoder[Bar] =
org.apache.spark.sql.Encoders.kryo[Bar]
}
которые могут использоваться вместе следующим образом:
object Main {
def main(args: Array[String]) {
val sc = new SparkContext("local", "test", new SparkConf())
val sqlContext = new SQLContext(sc)
import sqlContext.implicits._
import BarEncoders._
val ds = Seq(new Bar(1)).toDS
ds.show
sc.stop()
}
}
Он хранит объекты как столбец binary
, поэтому при преобразовании в DataFrame
вы получаете следующую схему:
root
|-- value: binary (nullable = true)
Также возможно кодировать кортежи с помощью kryo
encoder для определенного поля:
val longBarEncoder = Encoders.tuple(Encoders.scalaLong, Encoders.kryo[Bar])
spark.createDataset(Seq((1L, new Bar(1))))(longBarEncoder)
// org.apache.spark.sql.Dataset[(Long, Bar)] = [_1: bigint, _2: binary]
Обратите внимание, что мы не зависим от неявных кодеров здесь, но передаем кодировщик явно, поэтому это, скорее всего, не будет работать с методом toDS
.
-
Использование неявных преобразований:
Обеспечьте неявные преобразования между представлением, которое может быть закодировано и пользовательским классом, например:
object BarConversions {
implicit def toInt(bar: Bar): Int = bar.bar
implicit def toBar(i: Int): Bar = new Bar(i)
}
object Main {
def main(args: Array[String]) {
val sc = new SparkContext("local", "test", new SparkConf())
val sqlContext = new SQLContext(sc)
import sqlContext.implicits._
import BarConversions._
type EncodedBar = Int
val bars: RDD[EncodedBar] = sc.parallelize(Seq(new Bar(1)))
val barsDS = bars.toDS
barsDS.show
barsDS.map(_.bar).show
sc.stop()
}
}
Похожие вопросы:
Ответ 3
Вы можете использовать UDTRegistration, а затем Case Classes, Tuples и т.д.... все правильно работает с вашим Пользовательским типом!
Предположим, вы хотите использовать пользовательское Enum:
trait CustomEnum { def value:String }
case object Foo extends CustomEnum { val value = "F" }
case object Bar extends CustomEnum { val value = "B" }
object CustomEnum {
def fromString(str:String) = Seq(Foo, Bar).find(_.value == str).get
}
Зарегистрируйте его так:
// First define a UDT class for it:
class CustomEnumUDT extends UserDefinedType[CustomEnum] {
override def sqlType: DataType = org.apache.spark.sql.types.StringType
override def serialize(obj: CustomEnum): Any = org.apache.spark.unsafe.types.UTF8String.fromString(obj.value)
// Note that this will be a UTF8String type
override def deserialize(datum: Any): CustomEnum = CustomEnum.fromString(datum.toString)
override def userClass: Class[CustomEnum] = classOf[CustomEnum]
}
// Then Register the UDT Class!
// NOTE: you have to put this file into the org.apache.spark package!
UDTRegistration.register(classOf[CustomEnum].getName, classOf[CustomEnumUDT].getName)
Тогда ИСПОЛЬЗУЙТЕ ЭТО!
case class UsingCustomEnum(id:Int, en:CustomEnum)
val seq = Seq(
UsingCustomEnum(1, Foo),
UsingCustomEnum(2, Bar),
UsingCustomEnum(3, Foo)
).toDS()
seq.filter(_.en == Foo).show()
println(seq.collect())
Предположим, вы хотите использовать Полиморфную запись:
trait CustomPoly
case class FooPoly(id:Int) extends CustomPoly
case class BarPoly(value:String, secondValue:Long) extends CustomPoly
... и использовать его так:
case class UsingPoly(id:Int, poly:CustomPoly)
Seq(
UsingPoly(1, new FooPoly(1)),
UsingPoly(2, new BarPoly("Blah", 123)),
UsingPoly(3, new FooPoly(1))
).toDS
polySeq.filter(_.poly match {
case FooPoly(value) => value == 1
case _ => false
}).show()
Вы можете написать собственный UDT, который кодирует все в байты (я использую сериализацию Java здесь, но, вероятно, лучше использовать контекст Spark Kryo).
Сначала определите класс UDT:
class CustomPolyUDT extends UserDefinedType[CustomPoly] {
val kryo = new Kryo()
override def sqlType: DataType = org.apache.spark.sql.types.BinaryType
override def serialize(obj: CustomPoly): Any = {
val bos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(bos)
oos.writeObject(obj)
bos.toByteArray
}
override def deserialize(datum: Any): CustomPoly = {
val bis = new ByteArrayInputStream(datum.asInstanceOf[Array[Byte]])
val ois = new ObjectInputStream(bis)
val obj = ois.readObject()
obj.asInstanceOf[CustomPoly]
}
override def userClass: Class[CustomPoly] = classOf[CustomPoly]
}
Затем зарегистрируйте его:
// NOTE: The file you do this in has to be inside of the org.apache.spark package!
UDTRegistration.register(classOf[CustomPoly].getName, classOf[CustomPolyUDT].getName)
Тогда вы можете использовать его!
// As shown above:
case class UsingPoly(id:Int, poly:CustomPoly)
Seq(
UsingPoly(1, new FooPoly(1)),
UsingPoly(2, new BarPoly("Blah", 123)),
UsingPoly(3, new FooPoly(1))
).toDS
polySeq.filter(_.poly match {
case FooPoly(value) => value == 1
case _ => false
}).show()
Ответ 4
Кодеры работают более или менее одинаково в Spark2.0
. И Kryo
по-прежнему является рекомендуемым выбором serialization
.
Вы можете посмотреть следующий пример с искровой оболочкой
scala> import spark.implicits._
import spark.implicits._
scala> import org.apache.spark.sql.Encoders
import org.apache.spark.sql.Encoders
scala> case class NormalPerson(name: String, age: Int) {
| def aboutMe = s"I am ${name}. I am ${age} years old."
| }
defined class NormalPerson
scala> case class ReversePerson(name: Int, age: String) {
| def aboutMe = s"I am ${name}. I am ${age} years old."
| }
defined class ReversePerson
scala> val normalPersons = Seq(
| NormalPerson("Superman", 25),
| NormalPerson("Spiderman", 17),
| NormalPerson("Ironman", 29)
| )
normalPersons: Seq[NormalPerson] = List(NormalPerson(Superman,25), NormalPerson(Spiderman,17), NormalPerson(Ironman,29))
scala> val ds1 = sc.parallelize(normalPersons).toDS
ds1: org.apache.spark.sql.Dataset[NormalPerson] = [name: string, age: int]
scala> val ds2 = ds1.map(np => ReversePerson(np.age, np.name))
ds2: org.apache.spark.sql.Dataset[ReversePerson] = [name: int, age: string]
scala> ds1.show()
+---------+---+
| name|age|
+---------+---+
| Superman| 25|
|Spiderman| 17|
| Ironman| 29|
+---------+---+
scala> ds2.show()
+----+---------+
|name| age|
+----+---------+
| 25| Superman|
| 17|Spiderman|
| 29| Ironman|
+----+---------+
scala> ds1.foreach(p => println(p.aboutMe))
I am Ironman. I am 29 years old.
I am Superman. I am 25 years old.
I am Spiderman. I am 17 years old.
scala> val ds2 = ds1.map(np => ReversePerson(np.age, np.name))
ds2: org.apache.spark.sql.Dataset[ReversePerson] = [name: int, age: string]
scala> ds2.foreach(p => println(p.aboutMe))
I am 17. I am Spiderman years old.
I am 25. I am Superman years old.
I am 29. I am Ironman years old.
До сих пор в текущей области не было appropriate encoders
, поэтому наши лица не были закодированы как binary
. Но это изменится, если мы предоставим некоторые кодеры implicit
, используя сериализацию Kryo
.
// Provide Encoders
scala> implicit val normalPersonKryoEncoder = Encoders.kryo[NormalPerson]
normalPersonKryoEncoder: org.apache.spark.sql.Encoder[NormalPerson] = class[value[0]: binary]
scala> implicit val reversePersonKryoEncoder = Encoders.kryo[ReversePerson]
reversePersonKryoEncoder: org.apache.spark.sql.Encoder[ReversePerson] = class[value[0]: binary]
// Ecoders will be used since they are now present in Scope
scala> val ds3 = sc.parallelize(normalPersons).toDS
ds3: org.apache.spark.sql.Dataset[NormalPerson] = [value: binary]
scala> val ds4 = ds3.map(np => ReversePerson(np.age, np.name))
ds4: org.apache.spark.sql.Dataset[ReversePerson] = [value: binary]
// now all our persons show up as binary values
scala> ds3.show()
+--------------------+
| value|
+--------------------+
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
+--------------------+
scala> ds4.show()
+--------------------+
| value|
+--------------------+
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
+--------------------+
// Our instances still work as expected
scala> ds3.foreach(p => println(p.aboutMe))
I am Ironman. I am 29 years old.
I am Spiderman. I am 17 years old.
I am Superman. I am 25 years old.
scala> ds4.foreach(p => println(p.aboutMe))
I am 25. I am Superman years old.
I am 29. I am Ironman years old.
I am 17. I am Spiderman years old.
Ответ 5
В случае класса Java Bean это может быть полезно
import spark.sqlContext.implicits._
import org.apache.spark.sql.Encoders
implicit val encoder = Encoders.bean[MyClasss](classOf[MyClass])
Теперь вы можете просто прочитать dataFrame как пользовательский DataFrame
dataFrame.as[MyClass]
Это создаст собственный кодировщик классов, а не двоичный.
Ответ 6
Мои примеры будут на Java, но я не думаю, что это трудно адаптироваться к Scala.
Мне удалось успешно преобразовать RDD<Fruit>
в Dataset<Fruit>
с помощью spark.createDataset и Encoders.bean, пока Fruit
является простым Java Bean.
Шаг 1. Создайте простой Java Bean.
public class Fruit implements Serializable {
private String name = "default-fruit";
private String color = "default-color";
// AllArgsConstructor
public Fruit(String name, String color) {
this.name = name;
this.color = color;
}
// NoArgsConstructor
public Fruit() {
this("default-fruit", "default-color");
}
// ...create getters and setters for above fields
// you figure it out
}
Я бы придерживался классов с примитивными типами и String как поля перед тем, как люди DataBricks повышают свои кодеры. Если у вас есть класс с вложенным объектом, создайте еще один простой Java Bean, при этом все его поля будут сплющены, поэтому вы можете использовать преобразования RDD для сопоставления сложного типа с более простым. Уверен, это небольшая дополнительная работа, но я думаю, что это очень поможет в производительности, работающей с плоской схемой.
Шаг 2: Получите ваш набор данных из RDD
SparkSession spark = SparkSession.builder().getOrCreate();
JavaSparkContext jsc = new JavaSparkContext();
List<Fruit> fruitList = ImmutableList.of(
new Fruit("apple", "red"),
new Fruit("orange", "orange"),
new Fruit("grape", "purple"));
JavaRDD<Fruit> fruitJavaRDD = jsc.parallelize(fruitList);
RDD<Fruit> fruitRDD = fruitJavaRDD.rdd();
Encoder<Fruit> fruitBean = Encoders.bean(Fruit.class);
Dataset<Fruit> fruitDataset = spark.createDataset(rdd, bean);
И вуаля! Намочите, промойте, повторите.
Ответ 7
Для тех, кто может в моей ситуации, я тоже здесь отвечу.
Чтобы быть конкретным,
-
Я читал "Set typed data" из SQLContext. Таким образом, исходный формат данных - DataFrame.
val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1") sample.show()
+---+---+ | a| b| +---+---+ | 1|[1]| +---+---+
-
Затем преобразуйте его в RDD, используя rdd.map() с mutable.WrappedArray.
sample.rdd.map(r => (r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet)).collect().foreach(println)
Результат:
(1,Set(1))
Ответ 8
В дополнение к уже представленным предложениям, еще один вариант, который я недавно обнаружил, заключается в том, что вы можете объявить свой собственный класс, включая черту org.apache.spark.sql.catalyst.DefinedByConstructorParams
.
Это работает, если класс имеет конструктор, который использует типы, которые может понимать ExpressionEncoder, то есть примитивные значения и стандартные коллекции. Это может пригодиться, когда вы не можете объявить класс как класс case, но не хотите использовать Kryo для его кодирования каждый раз, когда он входит в набор данных.
Например, я хотел объявить класс case, который включал вектор Бриза. Единственный кодировщик, который мог бы обрабатывать это, как правило, Kryo. Но если я объявляю подкласс, который расширил Breeze DenseVector и DefinedByConstructorParams, ExpressionEncoder понял, что он может быть сериализован как массив из Doubles.
Вот как я это заявил:
class SerializableDenseVector(values: Array[Double]) extends breeze.linalg.DenseVector[Double](values) with DefinedByConstructorParams
implicit def BreezeVectorToSerializable(bv: breeze.linalg.DenseVector[Double]): SerializableDenseVector = bv.asInstanceOf[SerializableDenseVector]
Теперь я могу использовать SerializableDenseVector
в наборе данных (напрямую или как часть Продукта), используя простой ExpressionEncoder и no Kryo. Он работает так же, как Breeze DenseVector, но сериализуется как массив [Double].