DiSMEC++
weights.cpp
Go to the documentation of this file.
1 // Copyright (c) 2021, Aalto University, developed by Erik Schultheis
2 // All rights reserved.
3 //
4 // SPDX-License-Identifier: MIT
5 
6 #include "io/weights.h"
7 #include "io/common.h"
8 #include "model/model.h"
9 #include "spdlog/spdlog.h"
10 #include "io/numpy.h"
11 #include "utils/eigen_generic.h"
12 
13 using namespace dismec;
14 using namespace dismec::io::model;
15 
16 namespace {
22  template<class F>
23  void save_weights(const Model& model, F&& weight_callback) {
24  DenseRealVector buffer(model.num_features());
25  for (label_id_t label = model.labels_begin(); label < model.labels_end(); ++label) {
26  model.get_weights_for_label(label, buffer);
27  weight_callback(buffer);
28  }
29  }
30 
36  template<class F>
37  void load_weights(Model& target, F&& read_callback) {
38  DenseRealVector buffer(target.num_features());
39  for (label_id_t label = target.labels_begin(); label < target.labels_end(); ++label) {
40  read_callback(buffer);
41  target.set_weights_for_label(label, Model::WeightVectorIn{buffer});
42  }
43  }
44 }
45 
46 // -------------------------------------------------------------------------------
47 // dense weights in txt file
48 // -------------------------------------------------------------------------------
49 
50 
51 void io::model::save_dense_weights_txt(std::ostream& target, const Model& model)
52 {
53  save_weights(model, [&](const auto& data) {
54  io::write_vector_as_text(target, data) << '\n';
55  if(target.bad()) {
56  throw std::runtime_error("Error while writing weights");
57  }
58  });
59 }
60 
61 void io::model::load_dense_weights_txt(std::istream& source, Model& target)
62 {
63  load_weights(target, [&](auto& data) {
64  read_vector_from_text(source, data);
65  });
66 }
67 
68 // -------------------------------------------------------------------------------
69 // sparse weights in npy file
70 // -------------------------------------------------------------------------------
71 
72 
73 void io::model::save_dense_weights_npy(std::streambuf& target, const Model& model) {
74  bool col_major = false;
75  std::string description = io::make_npy_description(io::data_type_string<real_t>(), col_major, model.contained_labels(), model.num_features());
76  io::write_npy_header(target, description);
77  save_weights(model, [&](const DenseRealVector & data) {
78  binary_dump(target, data.data(), data.data() + data.size());
79  });
80 }
81 
82 void io::model::load_dense_weights_npy(std::streambuf& source, Model& target)
83 {
84  auto info = parse_npy_header(source);
85 
86  // verify that info is consistent
87  if(info.DataType != data_type_string<real_t>()) {
88  THROW_ERROR("Mismatch in data type, got {} but expected {}", info.DataType, data_type_string<real_t>());
89  }
90 
91  if(info.Cols != target.num_features()) {
92  THROW_ERROR("Weight data has {} columns, but model expects {} features", info.Cols, target.num_features());
93  }
94  if(info.Rows != target.contained_labels()) {
95  THROW_ERROR("Weight data has {} rows, but model expects {} labels", info.Rows, target.contained_labels());
96  }
97 
98  if(info.ColumnMajor) {
99  THROW_ERROR("Weight data is required to be in row-major format");
100  }
101 
102  load_weights(target, [&](DenseRealVector& data) {
103  binary_load(source, data.data(), data.data() + data.size());
104  });
105 }
106 
107 // -------------------------------------------------------------------------------
108 // sparse weights in txt file
109 // -------------------------------------------------------------------------------
110 
111 void io::model::save_as_sparse_weights_txt(std::ostream& target, const Model& model, double threshold)
112 {
113  if(threshold < 0) {
114  throw std::invalid_argument("Threshold cannot be negative");
115  }
116 
117  // TODO should we save the nnz on each line? could make reading as sparse more efficient
118  long nnz = 0;
119  save_weights(model, [&](const DenseRealVector& data) {
120  for(int j = 0; j < data.size(); ++j)
121  {
122  if(std::abs(data.coeff(j)) > threshold) {
123  target << j << ':' << data.coeff(j) << ' ';
124  ++nnz;
125  }
126  }
127  target << '\n';
128  });
129 
130  long entries = model.contained_labels() * model.num_features();
131  if(nnz > 0.25 * entries) {
132  spdlog::warn("Saved model in sparse mode, but sparsity is only {}%. "
133  "Consider increasing the threshold or saving as dense data.",
134  100 - (100 * nnz) / entries);
135  } else {
136  spdlog::info("Saved model in sparse mode. Only {:2.2}% of weights exceeded threshold.", double(100 * nnz) / entries);
137  }
138 }
139 
140 void io::model::load_sparse_weights_txt(std::istream& source, Model& target) {
141  Eigen::SparseVector<real_t> sparse_vec;
142  sparse_vec.resize(target.num_features());
143  std::string line_buffer;
144  long num_features = target.num_features();
145  for (label_id_t label = target.labels_begin(); label < target.labels_end(); ++label)
146  {
147  if(!std::getline(source, line_buffer)) {
148  THROW_ERROR("Input operation failed when trying to read weights for label {} out of {}",
149  label.to_index(), target.num_labels());
150  }
151  sparse_vec.setZero();
152  try {
153  io::parse_sparse_vector_from_text(line_buffer.data(), [&](long index, double value) {
154  if (index >= num_features || index < 0) {
155  THROW_ERROR("Encountered index {:5} with value {} for weights of label {:6}. Number of features "
156  "was specified as {}.", index, value, label.to_index(), num_features);
157  }
158  sparse_vec.insertBack(index) = value;
159  });
160  } catch (const std::exception& error) {
161  THROW_ERROR("Error while parsing weights for label {:6}: {}", label.to_index(), error.what());
162  }
163 
164  target.set_weights_for_label(label, Model::WeightVectorIn{sparse_vec});
165  }
166 }
167 
168 
169 #include "doctest.h"
170 #include "model/dense.h"
171 
172 using ::model::DenseModel;
173 using ::model::PartialModelSpec;
174 
180 TEST_CASE("save/load weights as plain text") {
181  // generate some fake data
182  DenseModel::WeightMatrix weights(2, 4);
183  weights << 1, 0, 0, 2,
184  0, 3, 0, -1;
185 
186  DenseModel model(std::make_shared<DenseModel::WeightMatrix>(weights), PartialModelSpec{label_id_t{1}, 4, 6});
187  DenseModel reconstruct(2, PartialModelSpec{label_id_t{1}, 4, 6});
188  std::stringstream target;
189 
190  std::string expected_dense = "1 0\n"
191  "0 3\n"
192  "0 0\n"
193  "2 -1\n";
194 
195  // note that we currently produce trailing whitespace here
196  std::string expected_sparse = "0:1 \n"
197  "1:3 \n"
198  "\n"
199  "0:2 1:-1 \n";
200 
201  SUBCASE("save dense txt") {
202  save_dense_weights_txt(target, model);
203  std::string result = target.str();
204  CHECK(result == expected_dense);
205  }
206 
207  SUBCASE("save sparse txt") {
208  save_as_sparse_weights_txt(target, model, 0.0);
209  std::string result = target.str();
210  CHECK(result == expected_sparse);
211  }
212 
213  SUBCASE("load dense txt") {
214  target.str(expected_dense);
215  load_dense_weights_txt(target, reconstruct);
216  CHECK(model.get_raw_weights() == reconstruct.get_raw_weights());
217  }
218 
219  SUBCASE("load sparse txt") {
220  target.str(expected_sparse);
221  load_sparse_weights_txt(target, reconstruct);
222  CHECK(model.get_raw_weights() == reconstruct.get_raw_weights());
223  }
224 }
225 
227 
228 TEST_CASE("save dense npy") {
229  DenseModel::WeightMatrix weights(2, 4);
230  weights << 1, 0, 0, 2,
231  0, 3, 0, -1;
232 
233  DenseModel model(std::make_shared<DenseModel::WeightMatrix>(weights), PartialModelSpec{label_id_t{1}, 4, 6});
234  DenseModel reconstruct(2, PartialModelSpec{label_id_t{1}, 4, 6});
235 
236  std::stringbuf target;
237  save_dense_weights_npy(target, model);
238  target.pubseekpos(0);
239  INFO(target.str());
240  load_dense_weights_npy(target, reconstruct);
241 
242  CHECK(model.get_raw_weights() == reconstruct.get_raw_weights());
243 }
Strong typedef for an int to signify a label id.
Definition: types.h:20
A model combines a set of weight with some meta-information about these weights.
Definition: model.h:63
label_id_t labels_end() const noexcept
Definition: model.h:102
virtual long num_features() const =0
How many weights are in each weight vector, i.e. how many features should the input have.
long num_labels() const noexcept
How many labels are in the underlying dataset.
Definition: model.h:78
long contained_labels() const noexcept
How many labels are in this submodel.
Definition: model.h:105
void set_weights_for_label(label_id_t label, const WeightVectorIn &weights)
Sets the weights for a label.
Definition: model.cpp:51
void get_weights_for_label(label_id_t label, Eigen::Ref< DenseRealVector > target) const
Gets the weights for the given label as a dense vector.
Definition: model.cpp:41
label_id_t labels_begin() const noexcept
Definition: model.h:98
building blocks for io procedures that are used by multiple io subsystems
#define THROW_ERROR(...)
Definition: common.h:23
Eigen::Matrix< real_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > WeightMatrix
Definition: numpy.cpp:13
void load_weights(Model &target, F &&read_callback)
Basic scaffold for loading weights.
Definition: weights.cpp:37
void save_weights(const Model &model, F &&weight_callback)
Basic scaffold for saving weights.
Definition: weights.cpp:23
namespace for all model-related io functions.
Definition: model-io.h:92
void load_sparse_weights_txt(std::istream &source, Model &target)
Loads sparse weights from plain-text format.
Definition: weights.cpp:140
void save_dense_weights_npy(std::streambuf &target, const Model &model)
Saves the dense weights in a npy file.
Definition: weights.cpp:73
void save_as_sparse_weights_txt(std::ostream &target, const Model &model, double threshold)
Saves the weights in sparse plain-text format, culling small weights.
Definition: weights.cpp:111
void load_dense_weights_txt(std::istream &source, Model &target)
Loads weights saved by io::model::save_dense_weights_txt.
Definition: weights.cpp:61
void save_dense_weights_txt(std::ostream &target, const Model &model)
Saves the dense weights in a plain-text format.
Definition: weights.cpp:51
void load_dense_weights_npy(std::streambuf &target, Model &model)
Loads dense weights from a npy file.
Definition: weights.cpp:82
std::ostream & write_vector_as_text(std::ostream &stream, const Eigen::Ref< const DenseRealVector > &data)
Writes the given vector as space-separated human-readable numbers.
Definition: common.cpp:21
std::istream & read_vector_from_text(std::istream &stream, Eigen::Ref< DenseRealVector > data)
Reads the given vector as space-separated human-readable numbers.
Definition: common.cpp:37
void binary_dump(std::streambuf &target, const T *begin, const T *end)
Definition: common.h:110
std::string make_npy_description(std::string_view dtype_desc, bool column_major, std::size_t size)
Creates a string with the data description dictionary for (1 dimensional) arrays.
Definition: numpy.cpp:48
void binary_load(std::streambuf &target, T *begin, T *end)
Definition: common.h:120
void write_npy_header(std::streambuf &target, std::string_view description)
Writes the header for a npy file.
Definition: numpy.cpp:32
void parse_sparse_vector_from_text(const char *feature_part, F &&callback)
parses sparse features given in index:value text format.
Definition: common.h:52
NpyHeaderData parse_npy_header(std::streambuf &source)
Parses the header of the npy file given by source.
Definition: numpy.cpp:280
Main namespace in which all types, classes, and functions are defined.
Definition: app.h:15
types::DenseVector< real_t > DenseRealVector
Any dense, real values vector.
Definition: matrix_types.h:40
TEST_CASE("save/load weights as plain text")
Definition: weights.cpp:180