| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from absl.testing import absltest |
| from absl.testing import parameterized |
| from flax_model.alphagenome._sdk.data import junction_data |
| from flax_model.alphagenome._sdk.data import track_data |
| from flax_model.alphagenome._sdk.models import dna_model |
| from flax_model.alphagenome.model import embeddings as embeddings_lib |
| from flax_model.alphagenome.model import model as model_lib |
| from flax_model.alphagenome.model import schemas |
| from flax_model.alphagenome.model.metadata import metadata as metadata_lib |
| import chex |
| import haiku as hk |
| import jax |
| import jax.numpy as jnp |
| from jaxtyping import Array, Float, Int |
| import numpy as np |
| import pandas as pd |
|
|
|
|
| def _mock_track_metadata( |
| num_tracks: int, num_padding: int = 0 |
| ) -> track_data.TrackMetadata: |
| return pd.DataFrame({ |
| 'name': ( |
| [f'track_{i}' for i in range(num_tracks)] + ['padding'] * num_padding |
| ), |
| 'nonzero_mean': [1.0] * num_tracks + [0.0] * num_padding, |
| }) |
|
|
|
|
| def _mock_junction_metadata( |
| num_tissues: int, num_padding: int = 0 |
| ) -> junction_data.JunctionMetadata: |
| return pd.DataFrame({ |
| 'tissue': ( |
| [f'tissue_{i}' for i in range(num_tissues)] |
| + ['padding'] * num_padding |
| ), |
| 'name': ( |
| [f'tissue_{i}' for i in range(num_tissues)] |
| + ['padding'] * num_padding |
| ), |
| }) |
|
|
|
|
| class ModelTest(parameterized.TestCase): |
|
|
| def test_create_model_metadata_consistency(self): |
| """Tests that the model raises an error if the metadata is inconsistent.""" |
| output_metadata_missing = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=13) |
| ), |
| dna_model.Organism.MUS_MUSCULUS: ( |
| metadata_lib.AlphaGenomeOutputMetadata() |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def forward(dna_sequence, organism_index): |
| return model_lib.AlphaGenome(output_metadata_missing)( |
| dna_sequence, organism_index |
| ) |
|
|
| with self.assertRaisesRegex( |
| ValueError, |
| r'No metadata found for output type "ATAC" for the following organisms:' |
| r'.*MUS_MUSCULUS.*', |
| ): |
| jax.eval_shape( |
| forward.init, |
| jax.random.key(0), |
| jax.ShapeDtypeStruct((1, 2048, 4), jnp.float32), |
| jax.ShapeDtypeStruct((1,), jnp.int32), |
| ) |
|
|
| @parameterized.named_parameters( |
| dict( |
| testcase_name='no_organisms', |
| organisms=(), |
| ), |
| dict( |
| testcase_name='one_organism', |
| organisms=(dna_model.Organism.HOMO_SAPIENS,), |
| ), |
| dict( |
| testcase_name='two_organisms', |
| organisms=( |
| dna_model.Organism.HOMO_SAPIENS, |
| dna_model.Organism.MUS_MUSCULUS, |
| ), |
| ), |
| ) |
| def test_create_headless_model( |
| self, organisms: tuple[dna_model.Organism, ...] |
| ): |
| """Tests that the model can be created without any heads.""" |
| output_metadata = { |
| organism: metadata_lib.AlphaGenomeOutputMetadata() |
| for organism in organisms |
| } |
|
|
| @hk.transform_with_state |
| def forward(dna_sequence, organism_index): |
| return model_lib.AlphaGenome(output_metadata)( |
| dna_sequence, organism_index |
| ) |
|
|
| params_shape, state_shape = jax.eval_shape( |
| forward.init, |
| jax.random.key(0), |
| seq_input := jax.ShapeDtypeStruct((1, 2048, 4), jnp.float32), |
| organism_index := jax.ShapeDtypeStruct((1,), jnp.int32), |
| ) |
| (predictions_shape, embeddings_shape), _ = jax.eval_shape( |
| forward.apply, |
| params_shape, |
| state_shape, |
| jax.random.key(0), |
| seq_input, |
| organism_index, |
| ) |
| |
| chex.assert_trees_all_equal_shapes( |
| predictions_shape, |
| {'embeddings_1bp': jnp.zeros((1, 2048, 1536))}, |
| ) |
| chex.assert_trees_all_equal_shapes( |
| embeddings_shape, |
| embeddings_lib.Embeddings( |
| embeddings_1bp=jnp.zeros((1, 2048, 1536)), |
| embeddings_128bp=jnp.zeros((1, 16, 3072), jnp.float32), |
| embeddings_pair=jnp.zeros((1, 1, 1, 128), jnp.float32), |
| ), |
| ) |
|
|
| def test_create_model(self): |
| num_tracks = 13 |
| output_metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=num_tracks) |
| ), |
| dna_model.Organism.MUS_MUSCULUS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=7, num_padding=num_tracks - 7) |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def forward( |
| dna_sequence: Float[Array, 'B S 4'], organism_index: Int[Array, 'B'] |
| ): |
| return model_lib.AlphaGenome(output_metadata)( |
| dna_sequence, organism_index |
| ) |
|
|
| seq_length = int(2**14) |
| key = jax.random.PRNGKey(42) |
| dna_sequence = jnp.zeros((1, seq_length, 4)) |
| organism_index = jnp.zeros((1,), dtype=jnp.int32) |
|
|
| params, state = forward.init(key, dna_sequence, organism_index) |
|
|
| (predictions, embeddings), _ = forward.apply( |
| params, state, key, dna_sequence, organism_index |
| ) |
| self.assertEqual(embeddings.embeddings_1bp.shape, (1, seq_length, 1536)) |
| self.assertEqual( |
| embeddings.embeddings_128bp.shape, (1, seq_length // 128, 3072) |
| ) |
| self.assertEqual( |
| embeddings.embeddings_pair.shape, |
| (1, seq_length // 128 // 16, seq_length // 128 // 16, 128), |
| ) |
| self.assertEqual( |
| predictions['atac']['scaled_predictions_1bp'].shape, |
| (1, seq_length, num_tracks), |
| ) |
| self.assertEqual( |
| predictions['atac']['scaled_predictions_128bp'].shape, |
| (1, seq_length // 128, num_tracks), |
| ) |
|
|
| def test_predict_junctions_with_same_params(self): |
| seq_length = 2048 |
| num_tissues = 15 |
| num_splice_sites = 17 |
| output_metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| splice_sites=_mock_track_metadata(5), |
| splice_junctions=_mock_junction_metadata(num_tissues), |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def forward( |
| dna_sequence: Float[Array, 'B S 4'], organism_index: Int[Array, 'B'] |
| ): |
| return model_lib.AlphaGenome( |
| output_metadata, num_splice_sites=num_splice_sites |
| )(dna_sequence, organism_index) |
|
|
| key = jax.random.PRNGKey(42) |
| dna_sequence = jax.ShapeDtypeStruct((1, seq_length, 4), jnp.float32) |
| organism_index = jax.ShapeDtypeStruct((1,), jnp.int32) |
|
|
| params, state = jax.eval_shape( |
| forward.init, key, dna_sequence, organism_index |
| ) |
|
|
| (predictions, embeddings), _ = jax.eval_shape( |
| forward.apply, params, state, key, dna_sequence, organism_index |
| ) |
|
|
| @hk.transform_with_state |
| def predict_junctions_fn(embeddings, splice_site_positions, organism_index): |
| return model_lib.AlphaGenome( |
| output_metadata, num_splice_sites=num_splice_sites |
| ).predict_junctions(embeddings, splice_site_positions, organism_index) |
|
|
| splice_site_positions = predictions['splice_sites_junction'][ |
| 'splice_site_positions' |
| ] |
| self.assertEqual(splice_site_positions.shape, (1, 4, num_splice_sites)) |
| junction_predictions, _ = jax.eval_shape( |
| predict_junctions_fn.apply, |
| params, |
| state, |
| key, |
| embeddings.embeddings_1bp, |
| splice_site_positions, |
| organism_index, |
| ) |
| self.assertIsNotNone(junction_predictions) |
| self.assertEqual( |
| junction_predictions['predictions'].shape, |
| (1, num_splice_sites, num_splice_sites, num_tissues * 2), |
| ) |
| self.assertEqual( |
| junction_predictions['splice_junction_mask'].shape, |
| (1, num_splice_sites, num_splice_sites, num_tissues * 2), |
| ) |
|
|
| def test_loss_and_grads(self): |
| seq_length = 131072 |
| batch_size = 1 |
| num_splice_sites = 17 |
| num_tissues_junctions = 1 |
| output_metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=1), |
| contact_maps=_mock_track_metadata(num_tracks=1), |
| splice_sites=_mock_track_metadata(num_tracks=5), |
| splice_site_usage=_mock_track_metadata(num_tracks=4), |
| splice_junctions=_mock_junction_metadata( |
| num_tissues=num_tissues_junctions |
| ), |
| ) |
| } |
|
|
| batch = schemas.DataBatch( |
| dna_sequence=jnp.zeros((batch_size, seq_length, 4), dtype=jnp.float32), |
| organism_index=jnp.zeros((batch_size,), dtype=jnp.int32), |
| atac=jnp.zeros((batch_size, seq_length, 1), dtype=jnp.float32), |
| atac_mask=jnp.ones((batch_size, 1, 1), dtype=bool), |
| contact_maps=jnp.zeros( |
| (batch_size, seq_length // 2048, seq_length // 2048, 1), |
| dtype=jnp.float32, |
| ), |
| splice_sites=jnp.zeros((batch_size, seq_length, 5), dtype=bool), |
| splice_site_usage=jnp.zeros( |
| (batch_size, seq_length, 4), dtype=jnp.float32 |
| ), |
| splice_junctions=jnp.zeros( |
| ( |
| batch_size, |
| num_splice_sites, |
| num_splice_sites, |
| num_tissues_junctions * 2, |
| ), |
| dtype=jnp.float32, |
| ), |
| ) |
|
|
| @hk.transform_with_state |
| def forward(batch): |
| return model_lib.AlphaGenome( |
| output_metadata, num_splice_sites=num_splice_sites |
| ).loss(batch) |
|
|
| key = jax.random.PRNGKey(42) |
| params, state = jax.eval_shape(forward.init, key, batch) |
| (loss, scalars, predictions), _ = jax.eval_shape( |
| forward.apply, params, state, key, batch |
| ) |
| chex.assert_shape(loss, ()) |
| chex.assert_tree_shape(scalars, ()) |
| chex.assert_tree_shape_prefix(predictions, (batch_size,)) |
| self.assertContainsSubset( |
| [ |
| 'atac_loss', |
| 'contact_maps_loss', |
| 'splice_sites_classification_loss', |
| 'splice_sites_usage_loss', |
| 'splice_sites_junction_loss', |
| ], |
| scalars.keys(), |
| ) |
|
|
| def grads_fn(params, state, batch): |
| def _loss_fn(params): |
| (loss, scalars, predictions), new_state = forward.apply( |
| params, state, None, batch |
| ) |
| return loss, (scalars, predictions, new_state) |
|
|
| (loss, (scalars, predictions, new_state)), grads = jax.value_and_grad( |
| _loss_fn, has_aux=True |
| )(params) |
| return loss, scalars, new_state, predictions, grads |
|
|
| loss2, scalars2, state2, predictions2, grads = jax.eval_shape( |
| grads_fn, params, state, batch |
| ) |
| chex.assert_trees_all_equal_shapes(loss, loss2) |
| chex.assert_trees_all_equal_shapes(scalars, scalars2) |
| chex.assert_trees_all_equal_shapes(state, state2) |
| chex.assert_trees_all_equal_shapes(predictions, predictions2) |
| chex.assert_trees_all_equal_shapes(params, grads) |
|
|
| def test_finetuning_weights_merging(self): |
| """Tests that the weights can be merged for fine-tuning. |
| |
| We test this by creating a base model and a fine-tuning model, where the |
| fine-tuning model has fewer heads and tracks than the base model. We then |
| merge the weights of the base model and the fine-tuning model, and check |
| that the loss can be computed with the merged weights. |
| """ |
| seq_length = 131072 |
| batch_size = 1 |
| key = jax.random.key(0) |
|
|
| |
| base_metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=(atac_tracks_base := 5)), |
| rna_seq=_mock_track_metadata(num_tracks=(rna_seq_tracks_base := 3)), |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def base_forward(batch: schemas.DataBatch): |
| return model_lib.AlphaGenome(base_metadata).loss(batch) |
|
|
| base_batch = schemas.DataBatch( |
| dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32), |
| organism_index=np.zeros((batch_size,), dtype=np.int32), |
| atac=jnp.zeros( |
| (batch_size, seq_length, atac_tracks_base), dtype=np.float32 |
| ), |
| atac_mask=np.ones((batch_size, 1, atac_tracks_base), dtype=bool), |
| rna_seq=np.zeros( |
| (batch_size, seq_length, rna_seq_tracks_base), dtype=np.float32 |
| ), |
| rna_seq_mask=np.ones((batch_size, 1, rna_seq_tracks_base), dtype=bool), |
| ) |
| base_params, base_state = jax.eval_shape(base_forward.init, key, base_batch) |
|
|
| |
| ft_metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=(atac_tracks_ft := 7)), |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def ft_forward(batch: schemas.DataBatch): |
| return model_lib.AlphaGenome(ft_metadata).loss(batch) |
|
|
| ft_batch = schemas.DataBatch( |
| dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32), |
| organism_index=np.zeros((batch_size,), dtype=np.int32), |
| atac=jnp.zeros( |
| (batch_size, seq_length, atac_tracks_ft), dtype=np.float32 |
| ), |
| atac_mask=np.ones((batch_size, 1, atac_tracks_ft), dtype=bool), |
| ) |
| ft_params, ft_state = jax.eval_shape(ft_forward.init, key, ft_batch) |
|
|
| |
| def merge(base_weights, ft_weights): |
| ft_head_weights = hk.data_structures.filter( |
| lambda module_name, name, v: 'head' in module_name, ft_weights |
| ) |
| return hk.data_structures.merge(base_weights, ft_head_weights) |
|
|
| merged_params = merge(base_params, ft_params) |
| merged_state = merge(base_state, ft_state) |
|
|
| |
| (loss, scalars, predictions), _ = jax.eval_shape( |
| ft_forward.apply, merged_params, merged_state, key, ft_batch |
| ) |
| chex.assert_shape(loss, ()) |
| self.assertContainsSubset(['atac_loss'], scalars.keys()) |
| self.assertNotIn('rna_seq_loss', scalars.keys()) |
| chex.assert_tree_shape_prefix(predictions, (batch_size,)) |
| self.assertEqual( |
| predictions['atac']['scaled_predictions_1bp'].shape, |
| (batch_size, seq_length, atac_tracks_ft), |
| ) |
|
|
| def test_freeze_trunk_embeddings(self): |
| if jax.local_devices()[0].platform.lower() not in {'gpu', 'tpu'}: |
| self.skipTest('This test requires accelerator devices.') |
|
|
| seq_length, batch_size, key = 131072, 1, jax.random.key(0) |
|
|
| metadata = { |
| dna_model.Organism.HOMO_SAPIENS: metadata_lib.AlphaGenomeOutputMetadata( |
| atac=_mock_track_metadata(num_tracks=(atac_tracks_base := 5)), |
| rna_seq=_mock_track_metadata(num_tracks=(rna_seq_tracks_base := 3)), |
| ), |
| } |
|
|
| @hk.transform_with_state |
| def forward(batch: schemas.DataBatch): |
| return model_lib.AlphaGenome(metadata, freeze_trunk_embeddings=True).loss( |
| batch |
| ) |
|
|
| @jax.jit |
| def grads_fn(params, state, batch): |
| def loss_fn(params, state, batch): |
| (loss, _, _), _ = forward.apply(params, state, None, batch) |
| return loss |
|
|
| grads_fn = jax.grad(loss_fn) |
| return grads_fn(params, state, batch) |
|
|
| batch = schemas.DataBatch( |
| dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32), |
| organism_index=np.zeros((batch_size,), dtype=np.int32), |
| atac=jnp.zeros( |
| (batch_size, seq_length, atac_tracks_base), dtype=np.float32 |
| ), |
| atac_mask=np.ones((batch_size, 1, atac_tracks_base), dtype=bool), |
| rna_seq=np.zeros( |
| (batch_size, seq_length, rna_seq_tracks_base), dtype=np.float32 |
| ), |
| rna_seq_mask=np.ones((batch_size, 1, rna_seq_tracks_base), dtype=bool), |
| ) |
| batch = jax.device_put(batch, jax.local_devices()[0]) |
|
|
| params, state = jax.jit(forward.init)(key, batch) |
| grads = jax.jit(grads_fn)(params, state, batch) |
| chex.assert_trees_all_equal_shapes(params, grads) |
| grads_trunk = hk.data_structures.filter( |
| lambda module_name, name, v: 'head' not in module_name, grads |
| ) |
| grads_head = hk.data_structures.filter( |
| lambda module_name, name, v: 'head' in module_name, grads |
| ) |
| |
| chex.assert_trees_all_equal( |
| grads_trunk, |
| jax.tree.map(np.zeros_like, grads_trunk), |
| ) |
| |
| head_grad_sq_norm = jax.tree_util.tree_reduce( |
| lambda acc, x: acc + jnp.sum(jnp.square(x)), |
| grads_head, |
| initializer=0.0, |
| ) |
| self.assertGreater(head_grad_sq_norm, 0.0) |
|
|
|
|
| if __name__ == '__main__': |
| absltest.main() |
|
|